rbx.cp 0.13.4__py3-none-any.whl → 0.13.5__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/box/checkers.py +2 -9
- rbx/box/cli.py +0 -1
- rbx/box/code.py +27 -80
- rbx/box/environment.py +16 -6
- rbx/box/generators.py +26 -3
- rbx/box/global_package.py +1 -1
- rbx/box/header.py +13 -1
- rbx/box/package.py +0 -14
- rbx/box/setter_config.py +11 -0
- rbx/box/solutions.py +12 -4
- rbx/box/tasks.py +9 -4
- rbx/box/testing/testing_package.py +7 -2
- rbx/box/ui/screens/run_explorer.py +0 -8
- rbx/box/ui/utils/run_ui.py +7 -3
- rbx/box/ui/widgets/test_output_box.py +1 -1
- rbx/box/validators.py +3 -1
- rbx/grading/caching.py +64 -14
- rbx/grading/judge/program.py +268 -0
- rbx/grading/judge/sandbox.py +30 -193
- rbx/grading/judge/sandboxes/stupid_sandbox.py +232 -241
- rbx/grading/judge/sandboxes/tee.py +31 -0
- rbx/grading/steps.py +87 -199
- rbx/grading/steps_with_caching.py +15 -6
- rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
- rbx/resources/templates/rbx.h +43 -2
- rbx/testing_utils.py +7 -0
- rbx/utils.py +51 -1
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.5.dist-info}/METADATA +1 -1
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.5.dist-info}/RECORD +32 -34
- rbx/grading/judge/sandboxes/isolate.py +0 -695
- rbx/grading/judge/testiso.py +0 -54
- rbx/resources/envs/isolate.rbx.yml +0 -36
- rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.5.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.5.dist-info}/WHEEL +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.5.dist-info}/entry_points.txt +0 -0
rbx/grading/caching.py
CHANGED
@@ -88,15 +88,26 @@ def _check_digests(artifacts_list: List[GradingArtifacts]):
|
|
88
88
|
produced.add(id(output.digest))
|
89
89
|
|
90
90
|
|
91
|
-
def
|
92
|
-
|
91
|
+
def _build_artifact_with_digest_list(
|
92
|
+
artifacts_list: List[GradingArtifacts],
|
93
|
+
) -> List[GradingFileOutput]:
|
94
|
+
outputs = []
|
93
95
|
for artifacts in artifacts_list:
|
94
96
|
for output in artifacts.outputs:
|
95
97
|
if output.hash and output.digest is None:
|
96
98
|
output.digest = DigestHolder()
|
97
99
|
if output.digest is None:
|
98
100
|
continue
|
99
|
-
|
101
|
+
outputs.append(output)
|
102
|
+
return outputs
|
103
|
+
|
104
|
+
|
105
|
+
def _build_digest_list(artifacts_list: List[GradingArtifacts]) -> List[DigestHolder]:
|
106
|
+
outputs = _build_artifact_with_digest_list(artifacts_list)
|
107
|
+
digests = []
|
108
|
+
for output in outputs:
|
109
|
+
assert output.digest is not None
|
110
|
+
digests.append(output.digest)
|
100
111
|
return digests
|
101
112
|
|
102
113
|
|
@@ -115,27 +126,44 @@ def _build_fingerprint_list(
|
|
115
126
|
return fingerprints
|
116
127
|
|
117
128
|
|
118
|
-
def _maybe_check_integrity(output: GradingFileOutput):
|
129
|
+
def _maybe_check_integrity(output: GradingFileOutput, integrity_digest: str):
|
119
130
|
if not grading_context.should_check_integrity():
|
120
131
|
return
|
121
|
-
if
|
132
|
+
if not output.hash:
|
133
|
+
return
|
134
|
+
if output.dest is None or not output.dest.is_symlink() or not output.dest.is_file():
|
135
|
+
# Only makes sense if the file EXISTS and IS A SYMLINK pointing to an
|
136
|
+
# EXISTING storage file.
|
137
|
+
# If the storage file ceases to exist, we can simply evict from the cache.
|
122
138
|
return
|
123
|
-
if output.digest is None
|
139
|
+
if output.digest is None:
|
124
140
|
return
|
125
141
|
with output.dest.open('rb') as f:
|
126
|
-
|
127
|
-
if
|
142
|
+
output_digest = digest_cooperatively(f)
|
143
|
+
if output_digest != integrity_digest:
|
128
144
|
raise ValueError(
|
129
145
|
f'Cache was tampered with, file {output.dest} has changed since it was cached.\nPlease run `rbx clean` to reset the cache.'
|
130
146
|
)
|
131
147
|
|
132
148
|
|
133
|
-
def
|
149
|
+
def _check_digest_list_integrity(
|
150
|
+
artifacts_list: List[GradingArtifacts], integrity_digests: List[Optional[str]]
|
151
|
+
):
|
152
|
+
outputs = _build_artifact_with_digest_list(artifacts_list)
|
153
|
+
assert len(outputs) == len(integrity_digests)
|
154
|
+
for output, integrity_digest in zip(outputs, integrity_digests):
|
155
|
+
assert output.digest is not None
|
156
|
+
if integrity_digest is None:
|
157
|
+
continue
|
158
|
+
_maybe_check_integrity(output, integrity_digest)
|
159
|
+
|
160
|
+
|
161
|
+
def _build_output_fingerprint_list(
|
162
|
+
artifacts_list: List[GradingArtifacts],
|
163
|
+
) -> List[str]:
|
134
164
|
fingerprints = []
|
135
165
|
for artifacts in artifacts_list:
|
136
166
|
for output in artifacts.outputs:
|
137
|
-
if output.hash:
|
138
|
-
_maybe_check_integrity(output)
|
139
167
|
if output.dest is None or output.intermediate or output.hash:
|
140
168
|
continue
|
141
169
|
if not output.dest.is_file():
|
@@ -160,7 +188,10 @@ def _build_cache_fingerprint(
|
|
160
188
|
) -> CacheFingerprint:
|
161
189
|
digests = [digest.value for digest in _build_digest_list(artifacts_list)]
|
162
190
|
fingerprints = _build_fingerprint_list(artifacts_list, cacher)
|
163
|
-
output_fingerprints = _build_output_fingerprint_list(
|
191
|
+
output_fingerprints = _build_output_fingerprint_list(
|
192
|
+
artifacts_list,
|
193
|
+
)
|
194
|
+
|
164
195
|
logs = _build_logs_list(artifacts_list)
|
165
196
|
return CacheFingerprint(
|
166
197
|
digests=digests,
|
@@ -389,7 +420,10 @@ class DependencyCache:
|
|
389
420
|
if fingerprint is None:
|
390
421
|
return False
|
391
422
|
|
392
|
-
reference_fingerprint = _build_cache_fingerprint(
|
423
|
+
reference_fingerprint = _build_cache_fingerprint(
|
424
|
+
artifact_list,
|
425
|
+
self.cacher,
|
426
|
+
)
|
393
427
|
|
394
428
|
if not _fingerprints_match(fingerprint, reference_fingerprint):
|
395
429
|
self._evict_from_cache(key)
|
@@ -399,6 +433,11 @@ class DependencyCache:
|
|
399
433
|
self._evict_from_cache(key)
|
400
434
|
return False
|
401
435
|
|
436
|
+
# Check whether existing storage files were not tampered with.
|
437
|
+
_check_digest_list_integrity(
|
438
|
+
artifact_list,
|
439
|
+
fingerprint.digests,
|
440
|
+
)
|
402
441
|
reference_digests = _build_digest_list(artifact_list)
|
403
442
|
|
404
443
|
# Apply digest changes.
|
@@ -422,6 +461,10 @@ class DependencyCache:
|
|
422
461
|
for logs, reference_logs in zip(fingerprint.logs, reference_fingerprint.logs):
|
423
462
|
if logs.run is not None:
|
424
463
|
reference_logs.run = logs.run.model_copy(deep=True)
|
464
|
+
if logs.interactor_run is not None:
|
465
|
+
reference_logs.interactor_run = logs.interactor_run.model_copy(
|
466
|
+
deep=True
|
467
|
+
)
|
425
468
|
if logs.preprocess is not None:
|
426
469
|
reference_logs.preprocess = [
|
427
470
|
log.model_copy(deep=True) for log in logs.preprocess
|
@@ -448,4 +491,11 @@ class DependencyCache:
|
|
448
491
|
if not are_artifacts_ok(artifact_list, self.cacher):
|
449
492
|
return
|
450
493
|
|
451
|
-
|
494
|
+
reference_fingerprint = _build_cache_fingerprint(
|
495
|
+
artifact_list,
|
496
|
+
self.cacher,
|
497
|
+
)
|
498
|
+
self._store_in_cache(
|
499
|
+
key,
|
500
|
+
reference_fingerprint,
|
501
|
+
)
|
@@ -0,0 +1,268 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import os
|
3
|
+
import pathlib
|
4
|
+
import resource
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import threading
|
8
|
+
import typing
|
9
|
+
from enum import Enum
|
10
|
+
from time import monotonic
|
11
|
+
from typing import IO, Any, Dict, List, Optional, Union
|
12
|
+
|
13
|
+
import psutil
|
14
|
+
|
15
|
+
from rbx.utils import PathOrStr
|
16
|
+
|
17
|
+
FileLike = Union[PathOrStr, IO[bytes], int]
|
18
|
+
|
19
|
+
|
20
|
+
def _maybe_close_files(files):
|
21
|
+
for fobj in files:
|
22
|
+
if isinstance(fobj, int):
|
23
|
+
continue
|
24
|
+
fobj.close()
|
25
|
+
|
26
|
+
|
27
|
+
def _is_pathlike(obj: Any) -> bool:
|
28
|
+
return isinstance(obj, str) or isinstance(obj, pathlib.Path)
|
29
|
+
|
30
|
+
|
31
|
+
@dataclasses.dataclass
|
32
|
+
class ProgramIO:
|
33
|
+
input: FileLike = subprocess.PIPE
|
34
|
+
output: FileLike = subprocess.PIPE
|
35
|
+
stderr: FileLike = subprocess.PIPE
|
36
|
+
|
37
|
+
def get_file_objects(self):
|
38
|
+
if isinstance(self.input, int):
|
39
|
+
input_fobj = self.input
|
40
|
+
elif _is_pathlike(self.input):
|
41
|
+
input_fobj = pathlib.Path(typing.cast(str, self.input)).open('r')
|
42
|
+
else:
|
43
|
+
input_fobj = typing.cast(IO[bytes], self.input)
|
44
|
+
if isinstance(self.output, int):
|
45
|
+
output_fobj = self.output
|
46
|
+
elif _is_pathlike(self.output):
|
47
|
+
output_path = pathlib.Path(typing.cast(str, self.output))
|
48
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
49
|
+
output_fobj = output_path.open('w')
|
50
|
+
else:
|
51
|
+
output_fobj = typing.cast(IO[bytes], self.output)
|
52
|
+
if isinstance(self.stderr, int):
|
53
|
+
stderr_fobj = self.stderr
|
54
|
+
elif _is_pathlike(self.stderr):
|
55
|
+
stderr_path = pathlib.Path(typing.cast(str, self.stderr))
|
56
|
+
stderr_path.parent.mkdir(parents=True, exist_ok=True)
|
57
|
+
stderr_fobj = stderr_path.open('w')
|
58
|
+
else:
|
59
|
+
stderr_fobj = typing.cast(IO[bytes], self.stderr)
|
60
|
+
return input_fobj, output_fobj, stderr_fobj
|
61
|
+
|
62
|
+
|
63
|
+
@dataclasses.dataclass
|
64
|
+
class ProgramPipes:
|
65
|
+
input: Optional[IO[bytes]] = None
|
66
|
+
output: Optional[IO[bytes]] = None
|
67
|
+
stderr: Optional[IO[bytes]] = None
|
68
|
+
|
69
|
+
|
70
|
+
@dataclasses.dataclass
|
71
|
+
class ProgramParams:
|
72
|
+
io: ProgramIO = dataclasses.field(default_factory=ProgramIO)
|
73
|
+
chdir: Optional[pathlib.Path] = None
|
74
|
+
time_limit: Optional[float] = None # seconds
|
75
|
+
wall_time_limit: Optional[float] = None # seconds
|
76
|
+
memory_limit: Optional[int] = None # megabytes
|
77
|
+
fs_limit: Optional[int] = None # kilobytes
|
78
|
+
env: Dict[str, str] = dataclasses.field(default_factory=dict)
|
79
|
+
pgid: Optional[int] = None
|
80
|
+
|
81
|
+
|
82
|
+
def get_preexec_fn(params: ProgramParams):
|
83
|
+
def preexec_fn():
|
84
|
+
os.setpgid(0, params.pgid or 0)
|
85
|
+
if params.time_limit is not None:
|
86
|
+
time_limit_in_ms = int(params.time_limit * 1000)
|
87
|
+
rlimit_cpu = int((time_limit_in_ms + 999) // 1000)
|
88
|
+
resource.setrlimit(resource.RLIMIT_CPU, (rlimit_cpu, rlimit_cpu + 1))
|
89
|
+
if params.fs_limit is not None:
|
90
|
+
fs_limit = params.fs_limit * 1024 # in bytes
|
91
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (fs_limit + 1, fs_limit * 2))
|
92
|
+
|
93
|
+
return preexec_fn
|
94
|
+
|
95
|
+
|
96
|
+
def get_memory_usage(ru: resource.struct_rusage) -> int:
|
97
|
+
if sys.platform == 'darwin':
|
98
|
+
return ru.ru_maxrss // 1024 + ru.ru_ixrss
|
99
|
+
return ru.ru_maxrss + ru.ru_ixrss + ru.ru_idrss + ru.ru_isrss
|
100
|
+
|
101
|
+
|
102
|
+
def get_cpu_time(ru: resource.struct_rusage) -> float:
|
103
|
+
return ru.ru_utime + ru.ru_stime
|
104
|
+
|
105
|
+
|
106
|
+
def get_file_sizes(io: ProgramIO):
|
107
|
+
return _get_file_size(io.output) + _get_file_size(io.stderr)
|
108
|
+
|
109
|
+
|
110
|
+
def _get_file_size(filename: Optional[FileLike]) -> int:
|
111
|
+
if filename is None or not _is_pathlike(filename):
|
112
|
+
return 0
|
113
|
+
path = pathlib.Path(typing.cast(str, filename))
|
114
|
+
if not path.is_file():
|
115
|
+
return 0
|
116
|
+
return path.stat().st_size
|
117
|
+
|
118
|
+
|
119
|
+
class ProgramCode(Enum):
|
120
|
+
RE = 'RE'
|
121
|
+
SG = 'SG'
|
122
|
+
TO = 'TO'
|
123
|
+
WT = 'WT'
|
124
|
+
ML = 'ML'
|
125
|
+
OL = 'OL'
|
126
|
+
TE = 'TE'
|
127
|
+
|
128
|
+
|
129
|
+
@dataclasses.dataclass
|
130
|
+
class ProgramResult:
|
131
|
+
exitcode: int
|
132
|
+
wall_time: float
|
133
|
+
cpu_time: float
|
134
|
+
memory_used: int
|
135
|
+
file_sizes: int
|
136
|
+
program_codes: List[ProgramCode]
|
137
|
+
killing_signal: Optional[int] = None
|
138
|
+
alarm_msg: Optional[str] = None
|
139
|
+
|
140
|
+
|
141
|
+
class Program:
|
142
|
+
def __init__(self, command: List[str], params: ProgramParams):
|
143
|
+
self.command = command
|
144
|
+
self.params = params
|
145
|
+
self.popen: Optional[subprocess.Popen] = None
|
146
|
+
self._files = []
|
147
|
+
|
148
|
+
self._stop_wall_handler = threading.Event()
|
149
|
+
self._stop_alarm_handler = threading.Event()
|
150
|
+
self._alarm_msg = ''
|
151
|
+
|
152
|
+
self._run()
|
153
|
+
|
154
|
+
@property
|
155
|
+
def pipes(self) -> ProgramPipes:
|
156
|
+
assert self.popen is not None
|
157
|
+
return ProgramPipes(
|
158
|
+
input=self.popen.stdin,
|
159
|
+
output=self.popen.stdout,
|
160
|
+
stderr=self.popen.stderr,
|
161
|
+
)
|
162
|
+
|
163
|
+
@property
|
164
|
+
def pid(self) -> int:
|
165
|
+
assert self.popen is not None
|
166
|
+
return self.popen.pid
|
167
|
+
|
168
|
+
def _kill_process(self):
|
169
|
+
if self.popen is not None:
|
170
|
+
self.popen.kill()
|
171
|
+
|
172
|
+
def _handle_wall(self):
|
173
|
+
if self._stop_wall_handler.wait(self.params.wall_time_limit):
|
174
|
+
return
|
175
|
+
self._stop_alarm_handler.set()
|
176
|
+
self._alarm_msg = 'wall timelimit'
|
177
|
+
self._kill_process()
|
178
|
+
|
179
|
+
def _handle_alarm(self):
|
180
|
+
if self._stop_alarm_handler.wait(0.3):
|
181
|
+
return
|
182
|
+
try:
|
183
|
+
process = psutil.Process(self.pid)
|
184
|
+
if self.params.time_limit is not None:
|
185
|
+
times = process.cpu_times()
|
186
|
+
cpu_time = times.user + times.system
|
187
|
+
if cpu_time > self.params.time_limit:
|
188
|
+
self._alarm_msg = 'timelimit'
|
189
|
+
self._kill_process()
|
190
|
+
if self.params.memory_limit is not None:
|
191
|
+
memory_info = process.memory_info()
|
192
|
+
memory_used = memory_info.rss
|
193
|
+
if memory_used > self.params.memory_limit * 1024 * 1024:
|
194
|
+
self._alarm_msg = 'memorylimit'
|
195
|
+
self._kill_process()
|
196
|
+
self._stop_alarm_handler.clear()
|
197
|
+
self._handle_alarm()
|
198
|
+
except psutil.NoSuchProcess:
|
199
|
+
return
|
200
|
+
|
201
|
+
def _run(self):
|
202
|
+
self._files = self.params.io.get_file_objects()
|
203
|
+
self.popen = subprocess.Popen(
|
204
|
+
self.command,
|
205
|
+
stdin=self._files[0],
|
206
|
+
stdout=self._files[1],
|
207
|
+
stderr=self._files[2],
|
208
|
+
cwd=self.params.chdir,
|
209
|
+
env={**os.environ, **self.params.env},
|
210
|
+
preexec_fn=get_preexec_fn(self.params),
|
211
|
+
close_fds=True,
|
212
|
+
)
|
213
|
+
self.start_time = monotonic()
|
214
|
+
|
215
|
+
threading.Thread(target=self._handle_wall, daemon=True).start()
|
216
|
+
threading.Thread(target=self._handle_alarm, daemon=True).start()
|
217
|
+
|
218
|
+
def process_exit(self, exitstatus, ru) -> ProgramResult:
|
219
|
+
wall_time = monotonic() - self.start_time
|
220
|
+
cpu_time = get_cpu_time(ru)
|
221
|
+
memory_used = get_memory_usage(ru)
|
222
|
+
file_sizes = get_file_sizes(self.params.io)
|
223
|
+
exitcode = os.waitstatus_to_exitcode(exitstatus)
|
224
|
+
killing_signal = None
|
225
|
+
program_codes = []
|
226
|
+
|
227
|
+
if exitcode < 0:
|
228
|
+
killing_signal = -exitcode
|
229
|
+
program_codes.append(ProgramCode.SG)
|
230
|
+
if exitcode > 0:
|
231
|
+
program_codes.append(ProgramCode.RE)
|
232
|
+
if self.params.time_limit is not None and (
|
233
|
+
cpu_time > self.params.time_limit or -exitcode == 24
|
234
|
+
):
|
235
|
+
program_codes.append(ProgramCode.TO)
|
236
|
+
if (
|
237
|
+
self.params.wall_time_limit is not None
|
238
|
+
and wall_time > self.params.wall_time_limit
|
239
|
+
):
|
240
|
+
program_codes.append(ProgramCode.WT)
|
241
|
+
program_codes.append(ProgramCode.TO)
|
242
|
+
if (
|
243
|
+
self.params.memory_limit is not None
|
244
|
+
and memory_used > self.params.memory_limit * 1024 * 1024
|
245
|
+
or self._alarm_msg == 'memorylimit'
|
246
|
+
):
|
247
|
+
program_codes.append(ProgramCode.ML)
|
248
|
+
if (
|
249
|
+
self.params.fs_limit is not None
|
250
|
+
and file_sizes > self.params.fs_limit * 1024
|
251
|
+
):
|
252
|
+
program_codes.append(ProgramCode.OL)
|
253
|
+
|
254
|
+
return ProgramResult(
|
255
|
+
exitcode=exitcode,
|
256
|
+
wall_time=wall_time,
|
257
|
+
cpu_time=cpu_time,
|
258
|
+
memory_used=memory_used,
|
259
|
+
file_sizes=file_sizes,
|
260
|
+
program_codes=program_codes,
|
261
|
+
killing_signal=killing_signal,
|
262
|
+
alarm_msg=self._alarm_msg or None,
|
263
|
+
)
|
264
|
+
|
265
|
+
def wait(self):
|
266
|
+
assert self.popen is not None
|
267
|
+
_, exitstatus, ru = os.wait4(self.pid, 0)
|
268
|
+
return self.process_exit(exitstatus, ru)
|
rbx/grading/judge/sandbox.py
CHANGED
@@ -3,16 +3,16 @@ import asyncio
|
|
3
3
|
import collections
|
4
4
|
import dataclasses
|
5
5
|
import io
|
6
|
+
import json
|
6
7
|
import logging
|
7
8
|
import os
|
8
9
|
import pathlib
|
9
10
|
import select
|
10
|
-
import signal
|
11
11
|
import stat
|
12
12
|
import subprocess
|
13
13
|
import sys
|
14
14
|
import typing
|
15
|
-
from typing import IO, Any, Dict, List, Optional
|
15
|
+
from typing import IO, Any, Dict, List, Optional, Tuple
|
16
16
|
|
17
17
|
import pydantic
|
18
18
|
|
@@ -210,6 +210,22 @@ class SandboxParams(pydantic.BaseModel):
|
|
210
210
|
self.dirs.append(DirectoryMount(src, dest, options))
|
211
211
|
|
212
212
|
|
213
|
+
class SandboxLog(pydantic.BaseModel):
|
214
|
+
"""A log of the sandbox."""
|
215
|
+
|
216
|
+
params: SandboxParams
|
217
|
+
execution_time: float # seconds
|
218
|
+
memory_used: int # bytes
|
219
|
+
exitcode: int
|
220
|
+
exitstatus: str
|
221
|
+
killing_signal: Optional[int] = None
|
222
|
+
exit_index: int = 0
|
223
|
+
other_logs: Dict[str, Any] = pydantic.Field(default_factory=dict)
|
224
|
+
|
225
|
+
def dump_other_logs(self) -> str:
|
226
|
+
return json.dumps(self.other_logs)
|
227
|
+
|
228
|
+
|
213
229
|
class SandboxBase(abc.ABC):
|
214
230
|
"""A base class for all sandboxes, meant to contain common
|
215
231
|
resources.
|
@@ -229,16 +245,12 @@ class SandboxBase(abc.ABC):
|
|
229
245
|
file_cacher: cacher.FileCacher
|
230
246
|
name: str
|
231
247
|
temp_dir: Optional[pathlib.Path]
|
232
|
-
cmd_file: pathlib.Path
|
233
|
-
|
234
|
-
params: SandboxParams
|
235
248
|
|
236
249
|
def __init__(
|
237
250
|
self,
|
238
251
|
file_cacher: Optional[cacher.FileCacher] = None,
|
239
252
|
name: Optional[str] = None,
|
240
253
|
temp_dir: Optional[pathlib.Path] = None,
|
241
|
-
params: Optional[SandboxParams] = None,
|
242
254
|
):
|
243
255
|
"""Initialization.
|
244
256
|
|
@@ -253,56 +265,7 @@ class SandboxBase(abc.ABC):
|
|
253
265
|
self.file_cacher = file_cacher or cacher.FileCacher(storage.NullStorage())
|
254
266
|
self.name = name if name is not None else 'unnamed'
|
255
267
|
self.temp_dir = temp_dir
|
256
|
-
|
257
|
-
self.cmd_file = pathlib.PosixPath('commands.log')
|
258
|
-
|
259
|
-
self.params = params or SandboxParams()
|
260
|
-
self.pid = None
|
261
|
-
self.pid_event = Event_ts()
|
262
|
-
|
263
|
-
# Set common environment variables.
|
264
|
-
# Specifically needed by Python, that searches the home for
|
265
|
-
# packages.
|
266
|
-
self.params.set_env['HOME'] = './'
|
267
|
-
|
268
|
-
def set_params(self, params: SandboxParams):
|
269
|
-
"""Set the parameters of the sandbox.
|
270
|
-
|
271
|
-
params (SandboxParams): the parameters to set.
|
272
|
-
|
273
|
-
"""
|
274
|
-
self.params = params
|
275
|
-
|
276
|
-
def set_multiprocess(self, multiprocess: bool):
|
277
|
-
"""Set the sandbox to (dis-)allow multiple threads and processes.
|
278
|
-
|
279
|
-
multiprocess (bool): whether to allow multiple thread/processes or not.
|
280
|
-
|
281
|
-
"""
|
282
|
-
if multiprocess:
|
283
|
-
# Max processes is set to 1000 to limit the effect of fork bombs.
|
284
|
-
self.params.max_processes = 1000
|
285
|
-
else:
|
286
|
-
self.params.max_processes = 1
|
287
|
-
|
288
|
-
def get_stats(self) -> str:
|
289
|
-
"""Return a human-readable string representing execution time
|
290
|
-
and memory usage.
|
291
|
-
|
292
|
-
return (string): human-readable stats.
|
293
|
-
|
294
|
-
"""
|
295
|
-
execution_time = self.get_execution_time()
|
296
|
-
if execution_time is not None:
|
297
|
-
time_str = f'{execution_time:.3f} sec'
|
298
|
-
else:
|
299
|
-
time_str = '(time unknown)'
|
300
|
-
memory_used = self.get_memory_used()
|
301
|
-
if memory_used is not None:
|
302
|
-
mem_str = f'{memory_used / (1024 * 1024):.2f} MB'
|
303
|
-
else:
|
304
|
-
mem_str = '(memory usage unknown)'
|
305
|
-
return f'[{time_str} - {mem_str}]'
|
268
|
+
self.cmd_file = pathlib.PosixPath('__commands__.log')
|
306
269
|
|
307
270
|
@abc.abstractmethod
|
308
271
|
def get_root_path(self) -> pathlib.Path:
|
@@ -313,101 +276,6 @@ class SandboxBase(abc.ABC):
|
|
313
276
|
"""
|
314
277
|
pass
|
315
278
|
|
316
|
-
@abc.abstractmethod
|
317
|
-
def get_execution_time(self) -> Optional[float]:
|
318
|
-
"""Return the time spent in the sandbox.
|
319
|
-
|
320
|
-
return (float): time spent in the sandbox.
|
321
|
-
|
322
|
-
"""
|
323
|
-
pass
|
324
|
-
|
325
|
-
@abc.abstractmethod
|
326
|
-
def get_memory_used(self) -> Optional[int]:
|
327
|
-
"""Return the memory used by the sandbox.
|
328
|
-
|
329
|
-
return (int): memory used by the sandbox (in bytes).
|
330
|
-
|
331
|
-
"""
|
332
|
-
pass
|
333
|
-
|
334
|
-
@abc.abstractmethod
|
335
|
-
def get_killing_signal(self) -> int:
|
336
|
-
"""Return the signal that killed the sandboxed process.
|
337
|
-
|
338
|
-
return (int): offending signal, or 0.
|
339
|
-
|
340
|
-
"""
|
341
|
-
pass
|
342
|
-
|
343
|
-
@abc.abstractmethod
|
344
|
-
def get_exit_status(self) -> str:
|
345
|
-
"""Get information about how the sandbox terminated.
|
346
|
-
|
347
|
-
return (string): the main reason why the sandbox terminated.
|
348
|
-
|
349
|
-
"""
|
350
|
-
pass
|
351
|
-
|
352
|
-
@abc.abstractmethod
|
353
|
-
def get_exit_code(self) -> int:
|
354
|
-
"""Return the exit code of the sandboxed process.
|
355
|
-
|
356
|
-
return (int): exitcode, or 0.
|
357
|
-
|
358
|
-
"""
|
359
|
-
pass
|
360
|
-
|
361
|
-
def set_pid(self, pid: int):
|
362
|
-
"""Set the PID of the sandboxed process.
|
363
|
-
|
364
|
-
pid (int): the PID of the sandboxed process.
|
365
|
-
|
366
|
-
"""
|
367
|
-
self.pid = pid
|
368
|
-
self.pid_event.set()
|
369
|
-
|
370
|
-
async def get_pid(self) -> int:
|
371
|
-
"""Return the PID of the sandboxed process.
|
372
|
-
|
373
|
-
Blocks until the PID is set.
|
374
|
-
|
375
|
-
return (int): the PID of the sandboxed process.
|
376
|
-
|
377
|
-
"""
|
378
|
-
await self.pid_event.wait()
|
379
|
-
assert self.pid is not None
|
380
|
-
return self.pid
|
381
|
-
|
382
|
-
def clear_pid(self):
|
383
|
-
"""Clear the PID of the sandboxed process."""
|
384
|
-
self.pid_event.clear()
|
385
|
-
self.pid = None
|
386
|
-
|
387
|
-
def use_pgid(self) -> bool:
|
388
|
-
"""Whether the sandbox supports process groups."""
|
389
|
-
return False
|
390
|
-
|
391
|
-
@abc.abstractmethod
|
392
|
-
def get_detailed_logs(self) -> str:
|
393
|
-
"""Return the detailed logs of the sandbox.
|
394
|
-
|
395
|
-
return (string): the detailed logs of the sandbox.
|
396
|
-
|
397
|
-
"""
|
398
|
-
pass
|
399
|
-
|
400
|
-
@abc.abstractmethod
|
401
|
-
def get_human_exit_description(self) -> str:
|
402
|
-
"""Get the status of the sandbox and return a human-readable
|
403
|
-
string describing it.
|
404
|
-
|
405
|
-
return (string): human-readable explaination of why the
|
406
|
-
sandbox terminated.
|
407
|
-
|
408
|
-
"""
|
409
|
-
pass
|
410
|
-
|
411
279
|
def use_soft_timeout(self) -> bool:
|
412
280
|
return False
|
413
281
|
|
@@ -702,52 +570,24 @@ class SandboxBase(abc.ABC):
|
|
702
570
|
]
|
703
571
|
|
704
572
|
@abc.abstractmethod
|
705
|
-
def
|
573
|
+
def run(
|
706
574
|
self,
|
707
575
|
command: List[str],
|
708
|
-
|
709
|
-
|
710
|
-
subprocess.Popen and discarding standard input, output and
|
711
|
-
error. More specifically, the standard input gets closed just
|
712
|
-
after the execution has started; standard output and error are
|
713
|
-
read until the end, in a way that prevents the execution from
|
714
|
-
being blocked because of insufficient buffering.
|
715
|
-
|
716
|
-
command ([string]): executable filename and arguments of the
|
717
|
-
command.
|
718
|
-
wait (bool): True if this call is blocking, False otherwise
|
719
|
-
|
720
|
-
return (bool|Popen): if the call is blocking, then return True
|
721
|
-
if the sandbox didn't report errors (caused by the sandbox
|
722
|
-
itself), False otherwise; if the call is not blocking,
|
723
|
-
return the Popen object from subprocess.
|
724
|
-
|
725
|
-
"""
|
576
|
+
params: SandboxParams,
|
577
|
+
) -> SandboxLog:
|
726
578
|
pass
|
727
579
|
|
728
580
|
@abc.abstractmethod
|
729
|
-
def
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
581
|
+
def run_communication(
|
582
|
+
self,
|
583
|
+
command: List[str],
|
584
|
+
params: SandboxParams,
|
585
|
+
interactor_command: List[str],
|
586
|
+
interactor_params: SandboxParams,
|
587
|
+
merged_capture: Optional[pathlib.Path] = None,
|
588
|
+
) -> Tuple[SandboxLog, SandboxLog]:
|
735
589
|
pass
|
736
590
|
|
737
|
-
def translate_box_exitcode(self, exitcode: int) -> bool:
|
738
|
-
"""Translate the sandbox exit code to a boolean sandbox success.
|
739
|
-
|
740
|
-
_ (int): the exit code of the sandbox.
|
741
|
-
|
742
|
-
return (bool): False if the sandbox had an error, True if it
|
743
|
-
terminated correctly (regardless of what the internal process
|
744
|
-
did).
|
745
|
-
|
746
|
-
"""
|
747
|
-
# SIGTERM can be safely ignored, just in case it leaks away from
|
748
|
-
# the sandbox.
|
749
|
-
return exitcode == 0 or exitcode == -signal.SIGTERM
|
750
|
-
|
751
591
|
@abc.abstractmethod
|
752
592
|
def initialize(self):
|
753
593
|
"""Initialize the sandbox.
|
@@ -779,9 +619,6 @@ class SandboxBase(abc.ABC):
|
|
779
619
|
"""
|
780
620
|
pass
|
781
621
|
|
782
|
-
def debug_message(self) -> Any:
|
783
|
-
return 'N/A'
|
784
|
-
|
785
622
|
|
786
623
|
class Truncator(io.RawIOBase):
|
787
624
|
"""Wrap a file-like object to simulate truncation.
|