rbx.cp 0.5.45__py3-none-any.whl → 0.5.47__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 +81 -28
- rbx/box/cli.py +12 -10
- rbx/box/generators.py +77 -40
- rbx/box/packaging/boca/packager.py +44 -7
- rbx/box/packaging/main.py +7 -0
- rbx/box/packaging/moj/packager.py +88 -8
- rbx/box/packaging/packager.py +7 -2
- rbx/box/packaging/polygon/packager.py +5 -4
- rbx/box/solutions.py +7 -5
- rbx/box/statements/builders.py +22 -3
- rbx/box/stresses.py +0 -1
- rbx/box/tasks.py +18 -8
- rbx/box/testcase_utils.py +66 -0
- rbx/grading/judge/sandbox.py +29 -1
- rbx/grading/judge/sandboxes/isolate.py +12 -2
- rbx/grading/judge/sandboxes/stupid_sandbox.py +17 -5
- rbx/grading/judge/sandboxes/timeit.py +12 -3
- rbx/grading/processing_context.py +48 -0
- rbx/grading/steps.py +24 -13
- rbx/resources/packagers/boca/checker.sh +8 -6
- rbx/resources/packagers/boca/compare.sh +48 -0
- rbx/resources/packagers/boca/interactive/c +207 -0
- rbx/resources/packagers/boca/interactive/cc +207 -0
- rbx/resources/packagers/boca/interactive/cpp +207 -0
- rbx/resources/packagers/boca/interactive/java +240 -0
- rbx/resources/packagers/boca/interactive/kt +231 -0
- rbx/resources/packagers/boca/interactive/py2 +209 -0
- rbx/resources/packagers/boca/interactive/py3 +209 -0
- rbx/resources/packagers/boca/interactor_compile.sh +45 -0
- rbx/resources/packagers/boca/run/bkp +163 -0
- rbx/resources/packagers/boca/run/c +19 -19
- rbx/resources/packagers/boca/run/cc +19 -19
- rbx/resources/packagers/boca/run/cpp +19 -19
- rbx/resources/packagers/boca/run/java +51 -51
- rbx/resources/packagers/boca/run/kt +30 -30
- rbx/resources/packagers/boca/run/py2 +42 -42
- rbx/resources/packagers/boca/run/py3 +42 -42
- rbx/resources/packagers/moj/scripts/c/compile.sh +19 -0
- rbx/resources/packagers/moj/scripts/c/prep.sh +5 -0
- rbx/resources/packagers/moj/scripts/c/run.sh +16 -0
- rbx/resources/packagers/moj/scripts/compare.sh +32 -6
- rbx/resources/packagers/moj/scripts/cpp/compile.sh +19 -0
- rbx/resources/packagers/moj/scripts/cpp/prep.sh +5 -0
- rbx/resources/packagers/moj/scripts/cpp/run.sh +16 -0
- rbx/resources/packagers/moj/scripts/interactor_prep.sh +14 -0
- rbx/resources/packagers/moj/scripts/interactor_run.sh +38 -0
- rbx/resources/packagers/moj/scripts/java/compile.sh +12 -0
- rbx/resources/packagers/moj/scripts/java/prep.sh +8 -0
- rbx/resources/packagers/moj/scripts/java/run.sh +17 -0
- rbx/resources/packagers/moj/scripts/py2/compile.sh +7 -0
- rbx/resources/packagers/moj/scripts/py2/prep.sh +5 -0
- rbx/resources/packagers/moj/scripts/py2/run.sh +16 -0
- rbx/resources/packagers/moj/scripts/py3/compile.sh +7 -0
- rbx/resources/packagers/moj/scripts/py3/prep.sh +5 -0
- rbx/resources/packagers/moj/scripts/py3/run.sh +16 -0
- {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/METADATA +1 -1
- {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/RECORD +60 -33
- rbx/resources/packagers/boca/compare +0 -53
- {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/entry_points.txt +0 -0
rbx/box/packaging/packager.py
CHANGED
@@ -7,7 +7,7 @@ from rbx.box import package
|
|
7
7
|
from rbx.box.contest import contest_package
|
8
8
|
from rbx.box.contest.schema import ContestProblem, ContestStatement
|
9
9
|
from rbx.box.generators import get_all_built_testcases
|
10
|
-
from rbx.box.schema import Package, Testcase, TestcaseGroup
|
10
|
+
from rbx.box.schema import Package, TaskType, Testcase, TestcaseGroup
|
11
11
|
from rbx.box.statements.schema import Statement, StatementType
|
12
12
|
|
13
13
|
|
@@ -33,10 +33,15 @@ class BuiltProblemPackage:
|
|
33
33
|
|
34
34
|
|
35
35
|
class BasePackager(ABC):
|
36
|
+
@classmethod
|
36
37
|
@abstractmethod
|
37
|
-
def name(
|
38
|
+
def name(cls) -> str:
|
38
39
|
pass
|
39
40
|
|
41
|
+
@classmethod
|
42
|
+
def task_types(cls) -> List[TaskType]:
|
43
|
+
return [TaskType.BATCH]
|
44
|
+
|
40
45
|
def languages(self):
|
41
46
|
pkg = package.find_problem_package_or_die()
|
42
47
|
|
@@ -188,15 +188,15 @@ class PolygonPackager(BasePackager):
|
|
188
188
|
for i, testcase in enumerate(testcases):
|
189
189
|
shutil.copyfile(
|
190
190
|
testcase.inputPath,
|
191
|
-
into_path / f'tests/{i+1:03d}',
|
191
|
+
into_path / f'tests/{i + 1:03d}',
|
192
192
|
)
|
193
193
|
if testcase.outputPath is not None:
|
194
194
|
shutil.copyfile(
|
195
195
|
testcase.outputPath,
|
196
|
-
into_path / f'tests/{i+1:03d}.a',
|
196
|
+
into_path / f'tests/{i + 1:03d}.a',
|
197
197
|
)
|
198
198
|
else:
|
199
|
-
(into_path / f'tests/{i+1:03d}.a').touch()
|
199
|
+
(into_path / f'tests/{i + 1:03d}.a').touch()
|
200
200
|
|
201
201
|
# Write problem.xml
|
202
202
|
(into_path / 'problem.xml').write_text(descriptor)
|
@@ -208,7 +208,8 @@ class PolygonPackager(BasePackager):
|
|
208
208
|
|
209
209
|
|
210
210
|
class PolygonContestPackager(BaseContestPackager):
|
211
|
-
|
211
|
+
@classmethod
|
212
|
+
def name(cls) -> str:
|
212
213
|
return 'polygon'
|
213
214
|
|
214
215
|
def _get_names(self) -> List[polygon_schema.Name]:
|
rbx/box/solutions.py
CHANGED
@@ -185,7 +185,7 @@ def _run_solution(
|
|
185
185
|
compiled_digest,
|
186
186
|
checker_digest,
|
187
187
|
testcase,
|
188
|
-
output_path,
|
188
|
+
output_dir=output_path,
|
189
189
|
interactor_digest=interactor_digest,
|
190
190
|
testcase_index=i,
|
191
191
|
verification=verification,
|
@@ -367,6 +367,7 @@ async def _generate_testcase_interactively(
|
|
367
367
|
)
|
368
368
|
|
369
369
|
is_manual = False
|
370
|
+
is_output_manual = False
|
370
371
|
generation_metadata = None
|
371
372
|
if generator is not None:
|
372
373
|
generation_metadata = GenerationMetadata(
|
@@ -398,6 +399,7 @@ async def _generate_testcase_interactively(
|
|
398
399
|
output = console.multiline_prompt('Testcase output')
|
399
400
|
testcase.outputPath.write_text(output)
|
400
401
|
console.console.print()
|
402
|
+
is_output_manual = True
|
401
403
|
|
402
404
|
generation_metadata = GenerationMetadata(
|
403
405
|
copied_to=testcase,
|
@@ -453,7 +455,7 @@ async def _generate_testcase_interactively(
|
|
453
455
|
)
|
454
456
|
raise
|
455
457
|
|
456
|
-
if main_solution_digest is not None:
|
458
|
+
if main_solution_digest is not None and not is_output_manual:
|
457
459
|
pkg = package.find_problem_package_or_die()
|
458
460
|
if pkg.type == TaskType.COMMUNICATION:
|
459
461
|
interactor_digest = checkers.compile_interactor(progress)
|
@@ -523,7 +525,7 @@ def _run_interactive_solutions(
|
|
523
525
|
compiled_solutions[solution.path],
|
524
526
|
checker_digest,
|
525
527
|
testcase,
|
526
|
-
output_dir,
|
528
|
+
output_dir=output_dir,
|
527
529
|
interactor_digest=interactor_digest,
|
528
530
|
verification=verification,
|
529
531
|
)
|
@@ -614,7 +616,7 @@ def _get_solution_repr(sol: Solution) -> List[Tuple[str, str]]:
|
|
614
616
|
]
|
615
617
|
|
616
618
|
|
617
|
-
def pick_solutions(tracked_solutions: Optional[Set[str]]) -> List[str]:
|
619
|
+
async def pick_solutions(tracked_solutions: Optional[Set[str]]) -> List[str]:
|
618
620
|
pkg = package.find_problem_package_or_die()
|
619
621
|
if tracked_solutions is None:
|
620
622
|
tracked_solutions = set(str(sol.path) for sol in pkg.solutions)
|
@@ -628,7 +630,7 @@ def pick_solutions(tracked_solutions: Optional[Set[str]]) -> List[str]:
|
|
628
630
|
if str(sol.path) in tracked_solutions
|
629
631
|
]
|
630
632
|
|
631
|
-
picked = questionary.checkbox('Select solutions', choices=choices).
|
633
|
+
picked = await questionary.checkbox('Select solutions', choices=choices).ask_async()
|
632
634
|
if picked is None:
|
633
635
|
raise typer.Abort()
|
634
636
|
return picked
|
rbx/box/statements/builders.py
CHANGED
@@ -25,6 +25,7 @@ from rbx.box.statements.schema import (
|
|
25
25
|
TexToPDF,
|
26
26
|
rbxToTeX,
|
27
27
|
)
|
28
|
+
from rbx.box.testcase_utils import TestcaseInteraction, parse_interaction
|
28
29
|
|
29
30
|
|
30
31
|
@dataclasses.dataclass
|
@@ -63,13 +64,31 @@ class StatementSample(BaseModel):
|
|
63
64
|
inputPath: pathlib.Path
|
64
65
|
outputPath: pathlib.Path
|
65
66
|
hasOutput: bool = True
|
67
|
+
interaction: Optional[TestcaseInteraction] = None
|
66
68
|
|
67
69
|
@staticmethod
|
68
70
|
def from_testcase(testcase: Testcase) -> 'StatementSample':
|
71
|
+
input_path = testcase.inputPath
|
72
|
+
output_path = testcase.outputPath
|
73
|
+
|
74
|
+
pin_path = input_path.with_suffix('.pin')
|
75
|
+
pout_path = input_path.with_suffix('.pout')
|
76
|
+
pio_path = input_path.with_suffix('.pio')
|
77
|
+
|
78
|
+
if pin_path.is_file():
|
79
|
+
input_path = pin_path
|
80
|
+
if pout_path.is_file():
|
81
|
+
output_path = pout_path
|
82
|
+
|
83
|
+
interaction = None
|
84
|
+
if pio_path.is_file():
|
85
|
+
interaction = parse_interaction(pio_path)
|
86
|
+
|
69
87
|
return StatementSample(
|
70
|
-
inputPath=
|
71
|
-
outputPath=
|
72
|
-
hasOutput=
|
88
|
+
inputPath=input_path,
|
89
|
+
outputPath=output_path or utils.get_empty_sentinel_path(),
|
90
|
+
hasOutput=output_path is not None,
|
91
|
+
interaction=interaction,
|
73
92
|
)
|
74
93
|
|
75
94
|
@staticmethod
|
rbx/box/stresses.py
CHANGED
rbx/box/tasks.py
CHANGED
@@ -42,7 +42,7 @@ async def run_solution_on_testcase(
|
|
42
42
|
compiled_digest: str,
|
43
43
|
checker_digest: Optional[str],
|
44
44
|
testcase: Testcase,
|
45
|
-
output_dir: pathlib.Path,
|
45
|
+
output_dir: Optional[pathlib.Path] = None,
|
46
46
|
interactor_digest: Optional[str] = None,
|
47
47
|
testcase_index: int = 0,
|
48
48
|
verification: VerificationLevel = VerificationLevel.NONE,
|
@@ -78,7 +78,11 @@ async def run_solution_on_testcase(
|
|
78
78
|
)
|
79
79
|
extra_config = _get_execution_config(limits, actual_sandbox)
|
80
80
|
|
81
|
-
|
81
|
+
if output_dir is None:
|
82
|
+
assert testcase.outputPath is not None
|
83
|
+
output_path = testcase.outputPath
|
84
|
+
else:
|
85
|
+
output_path = output_dir / testcase.inputPath.with_suffix('.out').name
|
82
86
|
error_path = output_path.with_suffix('.err')
|
83
87
|
log_path = output_path.with_suffix('.log')
|
84
88
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
@@ -151,7 +155,7 @@ async def _run_communication_solution_on_testcase(
|
|
151
155
|
interactor_digest: str,
|
152
156
|
checker_digest: Optional[str],
|
153
157
|
testcase: Testcase,
|
154
|
-
output_dir: pathlib.Path,
|
158
|
+
output_dir: Optional[pathlib.Path] = None,
|
155
159
|
testcase_index: int = 0,
|
156
160
|
verification: VerificationLevel = VerificationLevel.NONE,
|
157
161
|
timelimit_override: Optional[int] = None,
|
@@ -185,8 +189,13 @@ async def _run_communication_solution_on_testcase(
|
|
185
189
|
)
|
186
190
|
# TODO: maybe combine wall time limits?
|
187
191
|
|
188
|
-
|
189
|
-
|
192
|
+
if output_dir is None:
|
193
|
+
assert testcase.outputPath is not None
|
194
|
+
output_path = testcase.outputPath
|
195
|
+
else:
|
196
|
+
output_path = output_dir / testcase.inputPath.with_suffix('.out').name
|
197
|
+
solution_error_path = output_path.with_suffix('.sol.err')
|
198
|
+
interactor_error_path = output_path.with_suffix('.int.err')
|
190
199
|
log_path = output_path.with_suffix('.log')
|
191
200
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
192
201
|
|
@@ -196,7 +205,7 @@ async def _run_communication_solution_on_testcase(
|
|
196
205
|
interactor_item = CommunicationItem(
|
197
206
|
code=package.get_interactor(),
|
198
207
|
executable=DigestOrSource.create(interactor_digest),
|
199
|
-
stderr=DigestOrDest.create(
|
208
|
+
stderr=DigestOrDest.create(interactor_error_path),
|
200
209
|
extra_config=interactor_extra_config,
|
201
210
|
extra_args='interactor.in interactor.out',
|
202
211
|
inputs=[
|
@@ -222,6 +231,7 @@ async def _run_communication_solution_on_testcase(
|
|
222
231
|
solution_item = CommunicationItem(
|
223
232
|
code=solution,
|
224
233
|
executable=DigestOrSource.create(compiled_digest),
|
234
|
+
stderr=DigestOrDest.create(solution_error_path),
|
225
235
|
extra_config=extra_config,
|
226
236
|
capture=DigestOrDest.create(solution_capture_path)
|
227
237
|
if solution_capture_path
|
@@ -240,7 +250,7 @@ async def _run_communication_solution_on_testcase(
|
|
240
250
|
checker_digest,
|
241
251
|
run_log,
|
242
252
|
interactor_run_log,
|
243
|
-
|
253
|
+
interactor_error_path,
|
244
254
|
testcase,
|
245
255
|
output_path,
|
246
256
|
)
|
@@ -255,7 +265,7 @@ async def _run_communication_solution_on_testcase(
|
|
255
265
|
log=TestcaseLog(
|
256
266
|
**(run_log.model_dump() if run_log is not None else {}),
|
257
267
|
stdout_absolute_path=output_path.absolute(),
|
258
|
-
stderr_absolute_path=
|
268
|
+
stderr_absolute_path=solution_error_path.absolute(),
|
259
269
|
log_absolute_path=log_path.absolute(),
|
260
270
|
),
|
261
271
|
)
|
rbx/box/testcase_utils.py
CHANGED
@@ -97,6 +97,16 @@ class TestcaseData(BaseModel):
|
|
97
97
|
output: str
|
98
98
|
|
99
99
|
|
100
|
+
class TestcaseInteractionEntry(BaseModel):
|
101
|
+
data: str
|
102
|
+
pipe: int
|
103
|
+
|
104
|
+
|
105
|
+
class TestcaseInteraction(BaseModel):
|
106
|
+
entries: List[TestcaseInteractionEntry]
|
107
|
+
prefixes: Tuple[str, str]
|
108
|
+
|
109
|
+
|
100
110
|
def find_built_testcases(group: TestcaseGroup) -> List[Testcase]:
|
101
111
|
inputs = find_built_testcase_inputs(group)
|
102
112
|
|
@@ -143,3 +153,59 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
|
|
143
153
|
if output_path.is_file():
|
144
154
|
res.outputPath = output_path
|
145
155
|
return res
|
156
|
+
|
157
|
+
|
158
|
+
def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
|
159
|
+
entries = []
|
160
|
+
with file.open('r') as f:
|
161
|
+
try:
|
162
|
+
interactor_prefix = f.readline().strip()
|
163
|
+
solution_prefix = f.readline().strip()
|
164
|
+
except Exception:
|
165
|
+
console.console.print(
|
166
|
+
f'[error]Failed to read interaction file [item]{file}[/item]. Expected the first two lines to be the interactor and solution prefixes.[/error]'
|
167
|
+
)
|
168
|
+
raise typer.Exit(1) from None
|
169
|
+
|
170
|
+
rest = f.read()
|
171
|
+
start = 0
|
172
|
+
|
173
|
+
def _find_next_prefix(start: int) -> Optional[Tuple[int, int]]:
|
174
|
+
interactor_idx = rest.find(interactor_prefix, start)
|
175
|
+
solution_idx = rest.find(solution_prefix, start)
|
176
|
+
if interactor_idx == -1 and solution_idx == -1:
|
177
|
+
return None
|
178
|
+
if interactor_idx == -1:
|
179
|
+
return (solution_idx, solution_idx + len(solution_prefix))
|
180
|
+
if solution_idx == -1:
|
181
|
+
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
182
|
+
if interactor_idx < solution_idx:
|
183
|
+
return (interactor_idx, interactor_idx + len(interactor_prefix))
|
184
|
+
return (solution_idx, solution_idx + len(solution_prefix))
|
185
|
+
|
186
|
+
def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
|
187
|
+
prefix = _find_next_prefix(start)
|
188
|
+
if prefix is None:
|
189
|
+
return None
|
190
|
+
prefix_start, prefix_end = prefix
|
191
|
+
prefix = rest[prefix_start:prefix_end]
|
192
|
+
pipe = 1 if prefix == solution_prefix else 0
|
193
|
+
|
194
|
+
nxt = _find_next_prefix(prefix_end)
|
195
|
+
if nxt is None:
|
196
|
+
return (pipe, (prefix_end, len(rest)))
|
197
|
+
nxt_start, _ = nxt
|
198
|
+
return (pipe, (prefix_end, nxt_start))
|
199
|
+
|
200
|
+
while True:
|
201
|
+
block = _find_next_block()
|
202
|
+
if block is None:
|
203
|
+
break
|
204
|
+
pipe, (st, nd) = block
|
205
|
+
entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
|
206
|
+
start = nd
|
207
|
+
|
208
|
+
return TestcaseInteraction(
|
209
|
+
prefixes=(interactor_prefix, solution_prefix),
|
210
|
+
entries=entries,
|
211
|
+
)
|
rbx/grading/judge/sandbox.py
CHANGED
@@ -6,6 +6,7 @@ import logging
|
|
6
6
|
import os
|
7
7
|
import pathlib
|
8
8
|
import select
|
9
|
+
import signal
|
9
10
|
import stat
|
10
11
|
import subprocess
|
11
12
|
import sys
|
@@ -14,6 +15,7 @@ from typing import IO, Any, Dict, List, Optional
|
|
14
15
|
|
15
16
|
import pydantic
|
16
17
|
|
18
|
+
from rbx.grading import processing_context
|
17
19
|
from rbx.grading.judge import cacher, storage
|
18
20
|
|
19
21
|
logger = logging.getLogger(__name__)
|
@@ -187,6 +189,7 @@ class SandboxBase(abc.ABC):
|
|
187
189
|
|
188
190
|
EXIT_SANDBOX_ERROR = 'sandbox error'
|
189
191
|
EXIT_OK = 'ok'
|
192
|
+
EXIT_TERMINATED = 'terminated'
|
190
193
|
EXIT_SIGNAL = 'signal'
|
191
194
|
EXIT_TIMEOUT = 'timeout'
|
192
195
|
EXIT_TIMEOUT_WALL = 'wall timeout'
|
@@ -225,6 +228,7 @@ class SandboxBase(abc.ABC):
|
|
225
228
|
self.cmd_file = pathlib.PosixPath('commands.log')
|
226
229
|
|
227
230
|
self.params = params or SandboxParams()
|
231
|
+
self.pid = None
|
228
232
|
|
229
233
|
# Set common environment variables.
|
230
234
|
# Specifically needed by Python, that searches the home for
|
@@ -324,6 +328,28 @@ class SandboxBase(abc.ABC):
|
|
324
328
|
"""
|
325
329
|
pass
|
326
330
|
|
331
|
+
def set_pid(self, pid: int):
|
332
|
+
processing_context.add_to_processing_context(pid)
|
333
|
+
self.pid = pid
|
334
|
+
|
335
|
+
def get_pid(self) -> Optional[int]:
|
336
|
+
"""Return the PID of the sandboxed process.
|
337
|
+
|
338
|
+
return (int|None): the PID of the sandboxed process, or None if
|
339
|
+
the sandboxed process is not running.
|
340
|
+
|
341
|
+
"""
|
342
|
+
return self.pid
|
343
|
+
|
344
|
+
@abc.abstractmethod
|
345
|
+
def get_detailed_logs(self) -> str:
|
346
|
+
"""Return the detailed logs of the sandbox.
|
347
|
+
|
348
|
+
return (string): the detailed logs of the sandbox.
|
349
|
+
|
350
|
+
"""
|
351
|
+
pass
|
352
|
+
|
327
353
|
@abc.abstractmethod
|
328
354
|
def get_human_exit_description(self) -> str:
|
329
355
|
"""Get the status of the sandbox and return a human-readable
|
@@ -667,7 +693,9 @@ class SandboxBase(abc.ABC):
|
|
667
693
|
did).
|
668
694
|
|
669
695
|
"""
|
670
|
-
|
696
|
+
# SIGTERM can be safely ignored, just in case it leaks away from
|
697
|
+
# the sandbox.
|
698
|
+
return exitcode == 0 or exitcode == -signal.SIGTERM
|
671
699
|
|
672
700
|
@abc.abstractmethod
|
673
701
|
def initialize(self):
|
@@ -420,6 +420,7 @@ class IsolateSandbox(SandboxBase):
|
|
420
420
|
return (string): the main reason why the sandbox terminated.
|
421
421
|
|
422
422
|
"""
|
423
|
+
# TODO: figure out EXIT_TERMINATED
|
423
424
|
assert self.log is not None
|
424
425
|
status_list = self.get_status_list()
|
425
426
|
if 'XX' in status_list:
|
@@ -462,6 +463,14 @@ class IsolateSandbox(SandboxBase):
|
|
462
463
|
return 'Execution failed because the return code was nonzero'
|
463
464
|
return ''
|
464
465
|
|
466
|
+
def get_detailed_logs(self) -> str:
|
467
|
+
"""Return the detailed logs of the sandbox.
|
468
|
+
|
469
|
+
return (string): the detailed logs of the sandbox.
|
470
|
+
|
471
|
+
"""
|
472
|
+
return str(self.log)
|
473
|
+
|
465
474
|
def inner_absolute_path(self, path: pathlib.Path) -> pathlib.Path:
|
466
475
|
"""Translate from a relative path inside the sandbox to an
|
467
476
|
absolute path inside the sandbox.
|
@@ -556,7 +565,7 @@ class IsolateSandbox(SandboxBase):
|
|
556
565
|
)
|
557
566
|
except OSError:
|
558
567
|
logger.critical(
|
559
|
-
'Failed to execute program in sandbox
|
568
|
+
'Failed to execute program in sandbox with command: %s',
|
560
569
|
str(args),
|
561
570
|
exc_info=True,
|
562
571
|
)
|
@@ -602,6 +611,7 @@ class IsolateSandbox(SandboxBase):
|
|
602
611
|
# std*** to interfere with command. Otherwise we let the
|
603
612
|
# caller handle these issues.
|
604
613
|
with popen as p:
|
614
|
+
self.set_pid(p.pid)
|
605
615
|
exitcode = self.translate_box_exitcode(
|
606
616
|
wait_without_std([p], actually_pipe_to_stdout=self.debug)[0]
|
607
617
|
)
|
@@ -680,4 +690,4 @@ class IsolateSandbox(SandboxBase):
|
|
680
690
|
if delete:
|
681
691
|
logger.debug('Deleting sandbox in %s.', self._outer_dir)
|
682
692
|
# Delete the working directory.
|
683
|
-
shutil.rmtree(str(self._outer_dir))
|
693
|
+
shutil.rmtree(str(self._outer_dir), ignore_errors=True)
|
@@ -57,7 +57,6 @@ class StupidSandbox(SandboxBase):
|
|
57
57
|
self.initialize()
|
58
58
|
|
59
59
|
self.exec_num = -1
|
60
|
-
self.popen = None
|
61
60
|
self.log = None
|
62
61
|
self.returncode = None
|
63
62
|
|
@@ -192,9 +191,13 @@ class StupidSandbox(SandboxBase):
|
|
192
191
|
return (string): the main reason why the sandbox terminated.
|
193
192
|
|
194
193
|
"""
|
195
|
-
if self.returncode
|
194
|
+
if self.returncode is not None and not self.translate_box_exitcode(
|
195
|
+
self.returncode
|
196
|
+
):
|
196
197
|
return self.EXIT_SANDBOX_ERROR
|
197
198
|
status_list = self.get_status_list()
|
199
|
+
if 'TE' in status_list:
|
200
|
+
return self.EXIT_TERMINATED
|
198
201
|
if 'WT' in status_list:
|
199
202
|
return self.EXIT_TIMEOUT_WALL
|
200
203
|
if 'TO' in status_list:
|
@@ -218,6 +221,9 @@ class StupidSandbox(SandboxBase):
|
|
218
221
|
assert self.log is not None
|
219
222
|
return int(self.log['exit-code'])
|
220
223
|
|
224
|
+
def get_detailed_logs(self) -> str:
|
225
|
+
return str(self.log)
|
226
|
+
|
221
227
|
def get_human_exit_description(self) -> str:
|
222
228
|
"""Get the status of the sandbox and return a human-readable
|
223
229
|
string describing it.
|
@@ -299,13 +305,19 @@ class StupidSandbox(SandboxBase):
|
|
299
305
|
+ self.get_timeit_args()
|
300
306
|
+ command
|
301
307
|
)
|
302
|
-
|
308
|
+
with subprocess.Popen(
|
303
309
|
real_command,
|
304
310
|
stdin=subprocess.PIPE,
|
305
311
|
stdout=subprocess.PIPE,
|
306
312
|
stderr=subprocess.STDOUT,
|
307
313
|
env={**os.environ, **self.params.set_env},
|
308
|
-
)
|
314
|
+
) as p:
|
315
|
+
self.set_pid(p.pid)
|
316
|
+
try:
|
317
|
+
self.returncode = p.wait()
|
318
|
+
except Exception:
|
319
|
+
p.kill()
|
320
|
+
raise
|
309
321
|
self.hydrate_logs()
|
310
322
|
return self.translate_box_exitcode(self.returncode)
|
311
323
|
|
@@ -321,4 +333,4 @@ class StupidSandbox(SandboxBase):
|
|
321
333
|
# This sandbox doesn't have any cleanup, but we might want to delete.
|
322
334
|
if delete:
|
323
335
|
logger.debug('Deleting sandbox in %s.', self._path)
|
324
|
-
shutil.rmtree(str(self._path))
|
336
|
+
shutil.rmtree(str(self._path), ignore_errors=True)
|
@@ -223,6 +223,7 @@ def wait_and_finish(
|
|
223
223
|
pid: int,
|
224
224
|
options: Options,
|
225
225
|
start_time: float,
|
226
|
+
status_holder: Set[str],
|
226
227
|
alarm_msg: Optional[List[Optional[str]]] = None,
|
227
228
|
):
|
228
229
|
_, exitstatus, ru = os.wait4(pid, 0)
|
@@ -237,7 +238,7 @@ def wait_and_finish(
|
|
237
238
|
if exitcode < 0:
|
238
239
|
entries.append(f'exit-sig: {-exitcode}')
|
239
240
|
|
240
|
-
status =
|
241
|
+
status = status_holder
|
241
242
|
if exitcode > 0:
|
242
243
|
status.add('RE')
|
243
244
|
if exitcode < 0:
|
@@ -284,9 +285,11 @@ def main():
|
|
284
285
|
os.chdir(options.chdir)
|
285
286
|
set_rlimits(options)
|
286
287
|
redirect_fds(options)
|
288
|
+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
287
289
|
os.execvp(options.argv[0], options.argv)
|
288
290
|
|
289
291
|
alarm_msg: List[Optional[str]] = [None]
|
292
|
+
status_holder: Set[str] = set()
|
290
293
|
|
291
294
|
def handle_alarm(*args, **kwargs):
|
292
295
|
nonlocal alarm_msg
|
@@ -311,15 +314,21 @@ def main():
|
|
311
314
|
|
312
315
|
signal.setitimer(signal.ITIMER_REAL, 0.3)
|
313
316
|
|
317
|
+
def handle_sub_term(*args, **kwargs):
|
318
|
+
nonlocal status_holder
|
319
|
+
status_holder.add('TE')
|
320
|
+
os.kill(sub_pid, 9)
|
321
|
+
|
314
322
|
signal.setitimer(signal.ITIMER_REAL, 0.3)
|
315
323
|
signal.signal(signal.SIGALRM, handle_alarm)
|
316
|
-
|
324
|
+
signal.signal(signal.SIGTERM, handle_sub_term)
|
317
325
|
|
326
|
+
wait_and_finish(sub_pid, options, start_time, status_holder, alarm_msg=alarm_msg)
|
318
327
|
# Cancel alarm before exiting to avoid surprises.
|
319
328
|
signal.setitimer(signal.ITIMER_REAL, 0)
|
320
329
|
|
321
330
|
# Exit gracefully.
|
322
|
-
sys.exit()
|
331
|
+
sys.exit(0)
|
323
332
|
|
324
333
|
|
325
334
|
if __name__ == '__main__':
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import contextlib
|
2
|
+
import os
|
3
|
+
import signal
|
4
|
+
import threading
|
5
|
+
from typing import Optional, Set
|
6
|
+
|
7
|
+
_processing_context_pids: Optional[Set[int]] = None
|
8
|
+
_lock = threading.Lock()
|
9
|
+
|
10
|
+
# Creating a processing context is not thread-safe, but adding to it is.
|
11
|
+
|
12
|
+
|
13
|
+
@contextlib.contextmanager
|
14
|
+
def new_processing_context():
|
15
|
+
global _processing_context_pids
|
16
|
+
with _lock:
|
17
|
+
old_processing_context_pids = _processing_context_pids
|
18
|
+
_processing_context_pids = set()
|
19
|
+
try:
|
20
|
+
yield
|
21
|
+
finally:
|
22
|
+
with _lock:
|
23
|
+
_processing_context_pids = old_processing_context_pids
|
24
|
+
|
25
|
+
|
26
|
+
def get_processing_context() -> Set[int]:
|
27
|
+
with _lock:
|
28
|
+
return _processing_context_pids or set()
|
29
|
+
|
30
|
+
|
31
|
+
def add_to_processing_context(pid: int):
|
32
|
+
global _processing_context_pids
|
33
|
+
with _lock:
|
34
|
+
if _processing_context_pids is None:
|
35
|
+
return
|
36
|
+
_processing_context_pids.add(pid)
|
37
|
+
|
38
|
+
|
39
|
+
def terminate_all_processes_in_context():
|
40
|
+
with _lock:
|
41
|
+
if _processing_context_pids is None:
|
42
|
+
return
|
43
|
+
for pid in _processing_context_pids:
|
44
|
+
try:
|
45
|
+
os.kill(pid, signal.SIGTERM)
|
46
|
+
except OSError:
|
47
|
+
pass
|
48
|
+
_processing_context_pids.clear()
|