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,748 @@
|
|
1
|
+
import abc
|
2
|
+
import dataclasses
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import pathlib
|
7
|
+
import select
|
8
|
+
import stat
|
9
|
+
import subprocess
|
10
|
+
import sys
|
11
|
+
import typing
|
12
|
+
from typing import IO, Any, Dict, List, Optional
|
13
|
+
|
14
|
+
import pydantic
|
15
|
+
|
16
|
+
from rbx.grading.judge import cacher, storage
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
MERGE_STDERR = pathlib.PosixPath('/dev/stdout')
|
21
|
+
|
22
|
+
|
23
|
+
def wait_without_std(
|
24
|
+
procs: List[subprocess.Popen], actually_pipe_to_stdout: bool = False
|
25
|
+
) -> List[int]:
|
26
|
+
"""Wait for the conclusion of the processes in the list, avoiding
|
27
|
+
starving for input and output.
|
28
|
+
|
29
|
+
procs (list): a list of processes as returned by Popen.
|
30
|
+
|
31
|
+
return (list): a list of return codes.
|
32
|
+
|
33
|
+
"""
|
34
|
+
|
35
|
+
def get_to_consume():
|
36
|
+
"""Amongst stdout and stderr of list of processes, find the
|
37
|
+
ones that are alive and not closed (i.e., that may still want
|
38
|
+
to write to).
|
39
|
+
|
40
|
+
return (list): a list of open streams.
|
41
|
+
|
42
|
+
"""
|
43
|
+
to_consume = []
|
44
|
+
for process in procs:
|
45
|
+
if process.poll() is None: # If the process is alive.
|
46
|
+
if process.stdout and not process.stdout.closed:
|
47
|
+
to_consume.append(process.stdout)
|
48
|
+
if process.stderr and not process.stderr.closed:
|
49
|
+
to_consume.append(process.stderr)
|
50
|
+
return to_consume
|
51
|
+
|
52
|
+
# Close stdin; just saying stdin=None isn't ok, because the
|
53
|
+
# standard input would be obtained from the application stdin,
|
54
|
+
# that could interfere with the child process behaviour
|
55
|
+
for process in procs:
|
56
|
+
if process.stdin:
|
57
|
+
process.stdin.close()
|
58
|
+
|
59
|
+
# Read stdout and stderr to the end without having to block
|
60
|
+
# because of insufficient buffering (and without allocating too
|
61
|
+
# much memory). Unix specific.
|
62
|
+
to_consume = get_to_consume()
|
63
|
+
|
64
|
+
while len(to_consume) > 0:
|
65
|
+
to_read = select.select(to_consume, [], [], 1.0)[0]
|
66
|
+
for file_ in to_read:
|
67
|
+
consumed = file_.read(8 * 1024)
|
68
|
+
if actually_pipe_to_stdout:
|
69
|
+
sys.stdout.buffer.write(consumed)
|
70
|
+
sys.stdout.buffer.flush()
|
71
|
+
to_consume = get_to_consume()
|
72
|
+
|
73
|
+
return [process.wait() for process in procs]
|
74
|
+
|
75
|
+
|
76
|
+
@dataclasses.dataclass
|
77
|
+
class DirectoryMount:
|
78
|
+
src: pathlib.Path
|
79
|
+
dst: pathlib.Path
|
80
|
+
options: Optional[str] = None
|
81
|
+
|
82
|
+
|
83
|
+
class SandboxParams(pydantic.BaseModel):
|
84
|
+
"""Parameters for the sandbox.
|
85
|
+
|
86
|
+
box_id (int): the id of the sandbox.
|
87
|
+
fsize (int|None): maximum file size.
|
88
|
+
cgroup (bool): whether to use cgroups.
|
89
|
+
dirs ([string]): directories to mount.
|
90
|
+
preserve_env (bool): whether to preserve the environment.
|
91
|
+
inherit_env ([string]): environment variables to inherit.
|
92
|
+
set_env (Dict[string, string]): environment variables to set.
|
93
|
+
verbosity (int): verbosity level.
|
94
|
+
max_processes (int): maximum number of processes.
|
95
|
+
|
96
|
+
"""
|
97
|
+
|
98
|
+
fsize: Optional[int] = None # KiB
|
99
|
+
cgroup: bool = False
|
100
|
+
dirs: List[DirectoryMount] = []
|
101
|
+
preserve_env: bool = False
|
102
|
+
inherit_env: List[str] = []
|
103
|
+
set_env: Dict[str, str] = {}
|
104
|
+
verbosity: int = 0
|
105
|
+
max_processes: Optional[int] = 1
|
106
|
+
|
107
|
+
stdin_file: Optional[pathlib.Path] = None
|
108
|
+
stdout_file: Optional[pathlib.Path] = None
|
109
|
+
stderr_file: Optional[pathlib.Path] = None
|
110
|
+
stack_space: Optional[int] = None # MiB
|
111
|
+
address_space: Optional[int] = None # MiB
|
112
|
+
timeout: Optional[int] = None # ms
|
113
|
+
wallclock_timeout: Optional[int] = None # ms
|
114
|
+
extra_timeout: Optional[int] = None # ms
|
115
|
+
|
116
|
+
def get_cacheable_params(self) -> Dict[str, Any]:
|
117
|
+
return self.model_dump(mode='json', exclude_unset=True, exclude_none=True)
|
118
|
+
|
119
|
+
def set_stdio(
|
120
|
+
self,
|
121
|
+
stdin: Optional[pathlib.Path] = None,
|
122
|
+
stdout: Optional[pathlib.Path] = None,
|
123
|
+
):
|
124
|
+
"""Set the standard input/output files.
|
125
|
+
|
126
|
+
stdin (Path): standard input file.
|
127
|
+
stdout (Path): standard output file.
|
128
|
+
|
129
|
+
"""
|
130
|
+
self.stdin_file = stdin
|
131
|
+
self.stdout_file = stdout
|
132
|
+
|
133
|
+
def set_stdall(
|
134
|
+
self,
|
135
|
+
stdin: Optional[pathlib.Path] = None,
|
136
|
+
stdout: Optional[pathlib.Path] = None,
|
137
|
+
stderr: Optional[pathlib.Path] = None,
|
138
|
+
):
|
139
|
+
"""Set the standard input/output/error files.
|
140
|
+
|
141
|
+
stdin (Path): standard input file.
|
142
|
+
stdout (Path): standard output file.
|
143
|
+
stderr (Path): standard error file.
|
144
|
+
|
145
|
+
"""
|
146
|
+
self.stdin_file = stdin
|
147
|
+
self.stdout_file = stdout
|
148
|
+
self.stderr_file = stderr
|
149
|
+
|
150
|
+
def add_mapped_directory(
|
151
|
+
self,
|
152
|
+
src: pathlib.Path,
|
153
|
+
dest: Optional[pathlib.Path] = None,
|
154
|
+
options: Optional[str] = None,
|
155
|
+
ignore_if_not_existing: bool = False,
|
156
|
+
):
|
157
|
+
"""Add src to the directory to be mapped inside the sandbox.
|
158
|
+
|
159
|
+
src (Path): directory to make visible.
|
160
|
+
dest (Path|None): if not None, the path where to bind src.
|
161
|
+
options (str|None): if not None, isolate's directory rule options.
|
162
|
+
ignore_if_not_existing (bool): if True, ignore the mapping when src
|
163
|
+
does not exist (instead of having isolate terminate with an
|
164
|
+
error).
|
165
|
+
|
166
|
+
"""
|
167
|
+
if dest is None:
|
168
|
+
dest = src
|
169
|
+
if ignore_if_not_existing and not src.exists():
|
170
|
+
return
|
171
|
+
self.dirs.append(DirectoryMount(src, dest, options))
|
172
|
+
|
173
|
+
|
174
|
+
class SandboxBase(abc.ABC):
|
175
|
+
"""A base class for all sandboxes, meant to contain common
|
176
|
+
resources.
|
177
|
+
|
178
|
+
"""
|
179
|
+
|
180
|
+
EXIT_SANDBOX_ERROR = 'sandbox error'
|
181
|
+
EXIT_OK = 'ok'
|
182
|
+
EXIT_SIGNAL = 'signal'
|
183
|
+
EXIT_TIMEOUT = 'timeout'
|
184
|
+
EXIT_TIMEOUT_WALL = 'wall timeout'
|
185
|
+
EXIT_NONZERO_RETURN = 'nonzero return'
|
186
|
+
EXIT_MEMORY_LIMIT_EXCEEDED = 'memory limit exceeded'
|
187
|
+
EXIT_OUTPUT_LIMIT_EXCEEDED = 'output limit exceeded'
|
188
|
+
|
189
|
+
file_cacher: cacher.FileCacher
|
190
|
+
name: str
|
191
|
+
temp_dir: Optional[pathlib.Path]
|
192
|
+
cmd_file: pathlib.Path
|
193
|
+
|
194
|
+
params: SandboxParams
|
195
|
+
|
196
|
+
def __init__(
|
197
|
+
self,
|
198
|
+
file_cacher: Optional[cacher.FileCacher] = None,
|
199
|
+
name: Optional[str] = None,
|
200
|
+
temp_dir: Optional[pathlib.Path] = None,
|
201
|
+
params: Optional[SandboxParams] = None,
|
202
|
+
):
|
203
|
+
"""Initialization.
|
204
|
+
|
205
|
+
file_cacher (FileCacher): an instance of the FileCacher class
|
206
|
+
(to interact with FS), if the sandbox needs it.
|
207
|
+
name (string|None): name of the sandbox, which might appear in the
|
208
|
+
path and in system logs.
|
209
|
+
temp_dir (Path|None): temporary directory to use; if None, use the
|
210
|
+
default temporary directory.
|
211
|
+
|
212
|
+
"""
|
213
|
+
self.file_cacher = file_cacher or cacher.FileCacher(storage.NullStorage())
|
214
|
+
self.name = name if name is not None else 'unnamed'
|
215
|
+
self.temp_dir = temp_dir
|
216
|
+
|
217
|
+
self.cmd_file = pathlib.PosixPath('commands.log')
|
218
|
+
|
219
|
+
self.params = params or SandboxParams()
|
220
|
+
|
221
|
+
# Set common environment variables.
|
222
|
+
# Specifically needed by Python, that searches the home for
|
223
|
+
# packages.
|
224
|
+
self.params.set_env['HOME'] = './'
|
225
|
+
|
226
|
+
def set_params(self, params: SandboxParams):
|
227
|
+
"""Set the parameters of the sandbox.
|
228
|
+
|
229
|
+
params (SandboxParams): the parameters to set.
|
230
|
+
|
231
|
+
"""
|
232
|
+
self.params = params
|
233
|
+
|
234
|
+
def set_multiprocess(self, multiprocess: bool):
|
235
|
+
"""Set the sandbox to (dis-)allow multiple threads and processes.
|
236
|
+
|
237
|
+
multiprocess (bool): whether to allow multiple thread/processes or not.
|
238
|
+
|
239
|
+
"""
|
240
|
+
if multiprocess:
|
241
|
+
# Max processes is set to 1000 to limit the effect of fork bombs.
|
242
|
+
self.params.max_processes = 1000
|
243
|
+
else:
|
244
|
+
self.params.max_processes = 1
|
245
|
+
|
246
|
+
def get_stats(self) -> str:
|
247
|
+
"""Return a human-readable string representing execution time
|
248
|
+
and memory usage.
|
249
|
+
|
250
|
+
return (string): human-readable stats.
|
251
|
+
|
252
|
+
"""
|
253
|
+
execution_time = self.get_execution_time()
|
254
|
+
if execution_time is not None:
|
255
|
+
time_str = f'{execution_time:.3f} sec'
|
256
|
+
else:
|
257
|
+
time_str = '(time unknown)'
|
258
|
+
memory_used = self.get_memory_used()
|
259
|
+
if memory_used is not None:
|
260
|
+
mem_str = f'{memory_used / (1024 * 1024):.2f} MB'
|
261
|
+
else:
|
262
|
+
mem_str = '(memory usage unknown)'
|
263
|
+
return f'[{time_str} - {mem_str}]'
|
264
|
+
|
265
|
+
@abc.abstractmethod
|
266
|
+
def get_root_path(self) -> pathlib.Path:
|
267
|
+
"""Return the toplevel path of the sandbox.
|
268
|
+
|
269
|
+
return (Path): the root path.
|
270
|
+
|
271
|
+
"""
|
272
|
+
pass
|
273
|
+
|
274
|
+
@abc.abstractmethod
|
275
|
+
def get_execution_time(self) -> Optional[float]:
|
276
|
+
"""Return the time spent in the sandbox.
|
277
|
+
|
278
|
+
return (float): time spent in the sandbox.
|
279
|
+
|
280
|
+
"""
|
281
|
+
pass
|
282
|
+
|
283
|
+
@abc.abstractmethod
|
284
|
+
def get_memory_used(self) -> Optional[int]:
|
285
|
+
"""Return the memory used by the sandbox.
|
286
|
+
|
287
|
+
return (int): memory used by the sandbox (in bytes).
|
288
|
+
|
289
|
+
"""
|
290
|
+
pass
|
291
|
+
|
292
|
+
@abc.abstractmethod
|
293
|
+
def get_killing_signal(self) -> int:
|
294
|
+
"""Return the signal that killed the sandboxed process.
|
295
|
+
|
296
|
+
return (int): offending signal, or 0.
|
297
|
+
|
298
|
+
"""
|
299
|
+
pass
|
300
|
+
|
301
|
+
@abc.abstractmethod
|
302
|
+
def get_exit_status(self) -> str:
|
303
|
+
"""Get information about how the sandbox terminated.
|
304
|
+
|
305
|
+
return (string): the main reason why the sandbox terminated.
|
306
|
+
|
307
|
+
"""
|
308
|
+
pass
|
309
|
+
|
310
|
+
@abc.abstractmethod
|
311
|
+
def get_exit_code(self) -> int:
|
312
|
+
"""Return the exit code of the sandboxed process.
|
313
|
+
|
314
|
+
return (int): exitcode, or 0.
|
315
|
+
|
316
|
+
"""
|
317
|
+
pass
|
318
|
+
|
319
|
+
@abc.abstractmethod
|
320
|
+
def get_human_exit_description(self) -> str:
|
321
|
+
"""Get the status of the sandbox and return a human-readable
|
322
|
+
string describing it.
|
323
|
+
|
324
|
+
return (string): human-readable explaination of why the
|
325
|
+
sandbox terminated.
|
326
|
+
|
327
|
+
"""
|
328
|
+
pass
|
329
|
+
|
330
|
+
def use_soft_timeout(self) -> bool:
|
331
|
+
return False
|
332
|
+
|
333
|
+
def relative_path(self, path: pathlib.Path) -> pathlib.Path:
|
334
|
+
"""Translate from a relative path inside the sandbox to a
|
335
|
+
system path.
|
336
|
+
|
337
|
+
path (Path): relative path of the file inside the sandbox.
|
338
|
+
|
339
|
+
return (string): the absolute path.
|
340
|
+
|
341
|
+
"""
|
342
|
+
return self.get_root_path() / path
|
343
|
+
|
344
|
+
def create_file(
|
345
|
+
self, path: pathlib.Path, executable: bool = False, override: bool = False
|
346
|
+
) -> IO[bytes]:
|
347
|
+
"""Create an empty file in the sandbox and open it in write
|
348
|
+
binary mode.
|
349
|
+
|
350
|
+
path (Path): relative path of the file inside the sandbox.
|
351
|
+
executable (bool): to set permissions.
|
352
|
+
|
353
|
+
return (file): the file opened in write binary mode.
|
354
|
+
|
355
|
+
"""
|
356
|
+
if executable:
|
357
|
+
logger.debug('Creating executable file %s in sandbox.', path)
|
358
|
+
else:
|
359
|
+
logger.debug('Creating plain file %s in sandbox.', path)
|
360
|
+
real_path = self.relative_path(path)
|
361
|
+
if override:
|
362
|
+
real_path.unlink(missing_ok=True)
|
363
|
+
# Ensure directory exists.
|
364
|
+
real_path.parent.mkdir(parents=True, exist_ok=True)
|
365
|
+
try:
|
366
|
+
file_fd = os.open(str(real_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
367
|
+
file_ = open(file_fd, 'wb')
|
368
|
+
except OSError as e:
|
369
|
+
logger.error(
|
370
|
+
'Failed create file %s in sandbox. Unable to '
|
371
|
+
'evalulate this submission. This may be due to '
|
372
|
+
'cheating. %s',
|
373
|
+
real_path,
|
374
|
+
e,
|
375
|
+
exc_info=True,
|
376
|
+
)
|
377
|
+
raise
|
378
|
+
mod = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
|
379
|
+
if executable:
|
380
|
+
mod |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
381
|
+
os.chmod(str(real_path), mod)
|
382
|
+
return file_
|
383
|
+
|
384
|
+
def create_symlink(
|
385
|
+
self, path: pathlib.Path, from_path: pathlib.Path, override: bool = False
|
386
|
+
) -> Optional[pathlib.Path]:
|
387
|
+
real_path = self.relative_path(path)
|
388
|
+
if override:
|
389
|
+
real_path.unlink(missing_ok=True)
|
390
|
+
try:
|
391
|
+
real_path.symlink_to(from_path.resolve())
|
392
|
+
except NotImplementedError:
|
393
|
+
return None
|
394
|
+
return real_path
|
395
|
+
|
396
|
+
def create_file_from_storage(
|
397
|
+
self,
|
398
|
+
path: pathlib.Path,
|
399
|
+
digest: str,
|
400
|
+
executable: bool = False,
|
401
|
+
override: bool = False,
|
402
|
+
try_symlink: bool = False,
|
403
|
+
):
|
404
|
+
"""Write a file taken from FS in the sandbox.
|
405
|
+
|
406
|
+
path (Path): relative path of the file inside the sandbox.
|
407
|
+
digest (string): digest of the file in FS.
|
408
|
+
executable (bool): to set permissions.
|
409
|
+
|
410
|
+
"""
|
411
|
+
if try_symlink and executable:
|
412
|
+
symlink_path = self.file_cacher.path_for_symlink(digest)
|
413
|
+
if symlink_path is not None:
|
414
|
+
created = self.create_symlink(
|
415
|
+
path,
|
416
|
+
from_path=symlink_path,
|
417
|
+
override=override,
|
418
|
+
)
|
419
|
+
if created is not None:
|
420
|
+
created.chmod(0o755)
|
421
|
+
return
|
422
|
+
with self.create_file(path, executable, override=override) as dest_fobj:
|
423
|
+
self.file_cacher.get_file_to_fobj(digest, dest_fobj)
|
424
|
+
|
425
|
+
def create_file_from_bytes(
|
426
|
+
self,
|
427
|
+
path: pathlib.Path,
|
428
|
+
content: bytes,
|
429
|
+
executable: bool = False,
|
430
|
+
override: bool = False,
|
431
|
+
):
|
432
|
+
"""Write some data to a file in the sandbox.
|
433
|
+
|
434
|
+
path (Path): relative path of the file inside the sandbox.
|
435
|
+
content (bytes): what to write in the file.
|
436
|
+
executable (bool): to set permissions.
|
437
|
+
|
438
|
+
"""
|
439
|
+
with self.create_file(path, executable, override=override) as dest_fobj:
|
440
|
+
dest_fobj.write(content)
|
441
|
+
|
442
|
+
def create_file_from_other_file(
|
443
|
+
self,
|
444
|
+
path: pathlib.Path,
|
445
|
+
from_path: pathlib.Path,
|
446
|
+
executable: bool = False,
|
447
|
+
override: bool = False,
|
448
|
+
try_symlink: bool = False,
|
449
|
+
):
|
450
|
+
"""Write a file taken from FS in the sandbox.
|
451
|
+
|
452
|
+
path (Path): relative path of the file inside the sandbox.
|
453
|
+
digest (string): digest of the file in FS.
|
454
|
+
executable (bool): to set permissions.
|
455
|
+
|
456
|
+
"""
|
457
|
+
if try_symlink and executable:
|
458
|
+
created = self.create_symlink(
|
459
|
+
path,
|
460
|
+
from_path,
|
461
|
+
override=override,
|
462
|
+
)
|
463
|
+
if created is not None:
|
464
|
+
created.chmod(0o755)
|
465
|
+
return
|
466
|
+
with self.create_file(path, executable, override=override) as dest_fobj:
|
467
|
+
with from_path.open('rb') as src_fobj:
|
468
|
+
storage.copyfileobj(src_fobj, dest_fobj)
|
469
|
+
|
470
|
+
def create_file_from_string(
|
471
|
+
self,
|
472
|
+
path: pathlib.Path,
|
473
|
+
content: str,
|
474
|
+
executable: bool = False,
|
475
|
+
override: bool = False,
|
476
|
+
):
|
477
|
+
"""Write some data to a file in the sandbox.
|
478
|
+
|
479
|
+
path (Path): relative path of the file inside the sandbox.
|
480
|
+
content (str): what to write in the file.
|
481
|
+
executable (bool): to set permissions.
|
482
|
+
|
483
|
+
"""
|
484
|
+
return self.create_file_from_bytes(
|
485
|
+
path, content.encode('utf-8'), executable, override=override
|
486
|
+
)
|
487
|
+
|
488
|
+
def get_file(
|
489
|
+
self, path: pathlib.Path, trunc_len: Optional[int] = None
|
490
|
+
) -> IO[bytes]:
|
491
|
+
"""Open a file in the sandbox given its relative path.
|
492
|
+
|
493
|
+
path (Path): relative path of the file inside the sandbox.
|
494
|
+
trunc_len (int|None): if None, does nothing; otherwise, before
|
495
|
+
returning truncate it at the specified length.
|
496
|
+
|
497
|
+
return (file): the file opened in read binary mode.
|
498
|
+
|
499
|
+
"""
|
500
|
+
logger.debug(f'Retrieving file {path} from sandbox.')
|
501
|
+
real_path = self.relative_path(path)
|
502
|
+
file_ = real_path.open('rb')
|
503
|
+
if trunc_len is not None:
|
504
|
+
file_ = Truncator(file_, trunc_len)
|
505
|
+
return typing.cast(IO[bytes], file_)
|
506
|
+
|
507
|
+
def get_file_text(
|
508
|
+
self, path: pathlib.Path, trunc_len: Optional[int] = None
|
509
|
+
) -> IO[str]:
|
510
|
+
"""Open a file in the sandbox given its relative path, in text mode.
|
511
|
+
|
512
|
+
Assumes encoding is UTF-8. The caller must handle decoding errors.
|
513
|
+
|
514
|
+
path (Path): relative path of the file inside the sandbox.
|
515
|
+
trunc_len (int|None): if None, does nothing; otherwise, before
|
516
|
+
returning truncate it at the specified length.
|
517
|
+
|
518
|
+
return (file): the file opened in read binary mode.
|
519
|
+
|
520
|
+
"""
|
521
|
+
logger.debug('Retrieving text file %s from sandbox.', path)
|
522
|
+
real_path = self.relative_path(path)
|
523
|
+
file_ = real_path.open('rt', encoding='utf-8')
|
524
|
+
if trunc_len is not None:
|
525
|
+
file_ = Truncator(file_, trunc_len)
|
526
|
+
return typing.cast(IO[str], file_)
|
527
|
+
|
528
|
+
def get_file_to_bytes(
|
529
|
+
self, path: pathlib.Path, maxlen: Optional[int] = 1024
|
530
|
+
) -> bytes:
|
531
|
+
"""Return the content of a file in the sandbox given its
|
532
|
+
relative path.
|
533
|
+
|
534
|
+
path (Path): relative path of the file inside the sandbox.
|
535
|
+
maxlen (int): maximum number of bytes to read, or None if no
|
536
|
+
limit.
|
537
|
+
|
538
|
+
return (bytes): the content of the file up to maxlen bytes.
|
539
|
+
|
540
|
+
"""
|
541
|
+
with self.get_file(path) as file_:
|
542
|
+
if maxlen is None:
|
543
|
+
return file_.read()
|
544
|
+
else:
|
545
|
+
return file_.read(maxlen)
|
546
|
+
|
547
|
+
def get_file_to_string(
|
548
|
+
self, path: pathlib.Path, maxlen: Optional[int] = 1024
|
549
|
+
) -> str:
|
550
|
+
"""Return the content of a file in the sandbox given its
|
551
|
+
relative path.
|
552
|
+
|
553
|
+
path (Path): relative path of the file inside the sandbox.
|
554
|
+
maxlen (int): maximum number of bytes to read, or None if no
|
555
|
+
limit.
|
556
|
+
|
557
|
+
return (string): the content of the file up to maxlen bytes.
|
558
|
+
|
559
|
+
"""
|
560
|
+
return self.get_file_to_bytes(path, maxlen).decode('utf-8')
|
561
|
+
|
562
|
+
def get_file_to_storage(
|
563
|
+
self, path: pathlib.Path, description: str = '', trunc_len: Optional[int] = None
|
564
|
+
) -> str:
|
565
|
+
"""Put a sandbox file in FS and return its digest.
|
566
|
+
|
567
|
+
path (Path): relative path of the file inside the sandbox.
|
568
|
+
description (str): the description for FS.
|
569
|
+
trunc_len (int|None): if None, does nothing; otherwise, before
|
570
|
+
returning truncate it at the specified length.
|
571
|
+
|
572
|
+
return (str): the digest of the file.
|
573
|
+
|
574
|
+
"""
|
575
|
+
with self.get_file(path, trunc_len=trunc_len) as file_:
|
576
|
+
return self.file_cacher.put_file_from_fobj(file_, description)
|
577
|
+
|
578
|
+
def stat_file(self, path: pathlib.Path) -> os.stat_result:
|
579
|
+
"""Return the stats of a file in the sandbox.
|
580
|
+
|
581
|
+
path (Path): relative path of the file inside the sandbox.
|
582
|
+
|
583
|
+
return (stat_result): the stat results.
|
584
|
+
|
585
|
+
"""
|
586
|
+
return self.relative_path(path).stat()
|
587
|
+
|
588
|
+
def file_exists(self, path: pathlib.Path) -> bool:
|
589
|
+
"""Return if a file exists in the sandbox.
|
590
|
+
|
591
|
+
path (Path): relative path of the file inside the sandbox.
|
592
|
+
|
593
|
+
return (bool): if the file exists.
|
594
|
+
|
595
|
+
"""
|
596
|
+
return self.relative_path(path).exists()
|
597
|
+
|
598
|
+
def remove_file(self, path: pathlib.Path):
|
599
|
+
"""Delete a file in the sandbox.
|
600
|
+
|
601
|
+
path (Path): relative path of the file inside the sandbox.
|
602
|
+
|
603
|
+
"""
|
604
|
+
self.relative_path(path).unlink(missing_ok=True)
|
605
|
+
|
606
|
+
def glob(self, glob_expr: str) -> List[pathlib.Path]:
|
607
|
+
return [
|
608
|
+
path.relative_to(self.get_root_path())
|
609
|
+
for path in self.get_root_path().glob(glob_expr)
|
610
|
+
]
|
611
|
+
|
612
|
+
@abc.abstractmethod
|
613
|
+
def execute_without_std(
|
614
|
+
self,
|
615
|
+
command: List[str],
|
616
|
+
) -> bool:
|
617
|
+
"""Execute the given command in the sandbox using
|
618
|
+
subprocess.Popen and discarding standard input, output and
|
619
|
+
error. More specifically, the standard input gets closed just
|
620
|
+
after the execution has started; standard output and error are
|
621
|
+
read until the end, in a way that prevents the execution from
|
622
|
+
being blocked because of insufficient buffering.
|
623
|
+
|
624
|
+
command ([string]): executable filename and arguments of the
|
625
|
+
command.
|
626
|
+
wait (bool): True if this call is blocking, False otherwise
|
627
|
+
|
628
|
+
return (bool|Popen): if the call is blocking, then return True
|
629
|
+
if the sandbox didn't report errors (caused by the sandbox
|
630
|
+
itself), False otherwise; if the call is not blocking,
|
631
|
+
return the Popen object from subprocess.
|
632
|
+
|
633
|
+
"""
|
634
|
+
pass
|
635
|
+
|
636
|
+
@abc.abstractmethod
|
637
|
+
def hydrate_logs(self):
|
638
|
+
"""Fetch the results of the execution and hydrate logs.
|
639
|
+
|
640
|
+
This method should be called after the execution has
|
641
|
+
terminated, to hydrate logs and stuff.
|
642
|
+
"""
|
643
|
+
pass
|
644
|
+
|
645
|
+
def translate_box_exitcode(self, exitcode: int) -> bool:
|
646
|
+
"""Translate the sandbox exit code to a boolean sandbox success.
|
647
|
+
|
648
|
+
_ (int): the exit code of the sandbox.
|
649
|
+
|
650
|
+
return (bool): False if the sandbox had an error, True if it
|
651
|
+
terminated correctly (regardless of what the internal process
|
652
|
+
did).
|
653
|
+
|
654
|
+
"""
|
655
|
+
return exitcode == 0
|
656
|
+
|
657
|
+
@abc.abstractmethod
|
658
|
+
def initialize(self):
|
659
|
+
"""Initialize the sandbox.
|
660
|
+
|
661
|
+
To be called at the beginning of the execution.
|
662
|
+
|
663
|
+
"""
|
664
|
+
pass
|
665
|
+
|
666
|
+
@abc.abstractmethod
|
667
|
+
def cleanup(self, delete: bool = False):
|
668
|
+
"""Cleanup the sandbox.
|
669
|
+
|
670
|
+
To be called at the end of the execution, regardless of
|
671
|
+
whether the sandbox should be deleted or not.
|
672
|
+
|
673
|
+
delete (bool): if True, also delete get_root_path() and everything it
|
674
|
+
contains.
|
675
|
+
|
676
|
+
"""
|
677
|
+
pass
|
678
|
+
|
679
|
+
def debug_message(self) -> Any:
|
680
|
+
return 'N/A'
|
681
|
+
|
682
|
+
|
683
|
+
class Truncator(io.RawIOBase):
|
684
|
+
"""Wrap a file-like object to simulate truncation.
|
685
|
+
|
686
|
+
This file-like object provides read-only access to a limited prefix
|
687
|
+
of a wrapped file-like object. It provides a truncated version of
|
688
|
+
the file without ever touching the object on the filesystem.
|
689
|
+
|
690
|
+
This class is only able to wrap binary streams as it relies on the
|
691
|
+
readinto method which isn't provided by text (unicode) streams.
|
692
|
+
|
693
|
+
"""
|
694
|
+
|
695
|
+
def __init__(self, fobj, size):
|
696
|
+
"""Wrap fobj and give access to its first size bytes.
|
697
|
+
|
698
|
+
fobj (fileobj): a file-like object to wrap.
|
699
|
+
size (int): the number of bytes that will be accessible.
|
700
|
+
|
701
|
+
"""
|
702
|
+
self.fobj = fobj
|
703
|
+
self.size = size
|
704
|
+
|
705
|
+
def close(self):
|
706
|
+
"""See io.IOBase.close."""
|
707
|
+
self.fobj.close()
|
708
|
+
|
709
|
+
@property
|
710
|
+
def closed(self):
|
711
|
+
"""See io.IOBase.closed."""
|
712
|
+
return self.fobj.closed
|
713
|
+
|
714
|
+
def readable(self):
|
715
|
+
"""See io.IOBase.readable."""
|
716
|
+
return True
|
717
|
+
|
718
|
+
def seekable(self):
|
719
|
+
"""See io.IOBase.seekable."""
|
720
|
+
return True
|
721
|
+
|
722
|
+
def readinto(self, b):
|
723
|
+
"""See io.RawIOBase.readinto."""
|
724
|
+
# This is the main "trick": we clip (i.e. mask, reduce, slice)
|
725
|
+
# the given buffer so that it doesn't overflow into the area we
|
726
|
+
# want to hide (that is, out of the prefix) and then we forward
|
727
|
+
# it to the wrapped file-like object.
|
728
|
+
b = memoryview(b)[: max(0, self.size - self.fobj.tell())]
|
729
|
+
return self.fobj.readinto(b)
|
730
|
+
|
731
|
+
def seek(self, offset, whence=io.SEEK_SET):
|
732
|
+
"""See io.IOBase.seek."""
|
733
|
+
# We have to catch seeks relative to the end of the file and
|
734
|
+
# adjust them to the new "imposed" size.
|
735
|
+
if whence == io.SEEK_END:
|
736
|
+
if self.fobj.seek(0, io.SEEK_END) > self.size:
|
737
|
+
self.fobj.seek(self.size, io.SEEK_SET)
|
738
|
+
return self.fobj.seek(offset, io.SEEK_CUR)
|
739
|
+
else:
|
740
|
+
return self.fobj.seek(offset, whence)
|
741
|
+
|
742
|
+
def tell(self):
|
743
|
+
"""See io.IOBase.tell."""
|
744
|
+
return self.fobj.tell()
|
745
|
+
|
746
|
+
def write(self, _):
|
747
|
+
"""See io.RawIOBase.write."""
|
748
|
+
raise io.UnsupportedOperation('write')
|