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.
Files changed (61) hide show
  1. rbx/box/checkers.py +81 -28
  2. rbx/box/cli.py +12 -10
  3. rbx/box/generators.py +77 -40
  4. rbx/box/packaging/boca/packager.py +44 -7
  5. rbx/box/packaging/main.py +7 -0
  6. rbx/box/packaging/moj/packager.py +88 -8
  7. rbx/box/packaging/packager.py +7 -2
  8. rbx/box/packaging/polygon/packager.py +5 -4
  9. rbx/box/solutions.py +7 -5
  10. rbx/box/statements/builders.py +22 -3
  11. rbx/box/stresses.py +0 -1
  12. rbx/box/tasks.py +18 -8
  13. rbx/box/testcase_utils.py +66 -0
  14. rbx/grading/judge/sandbox.py +29 -1
  15. rbx/grading/judge/sandboxes/isolate.py +12 -2
  16. rbx/grading/judge/sandboxes/stupid_sandbox.py +17 -5
  17. rbx/grading/judge/sandboxes/timeit.py +12 -3
  18. rbx/grading/processing_context.py +48 -0
  19. rbx/grading/steps.py +24 -13
  20. rbx/resources/packagers/boca/checker.sh +8 -6
  21. rbx/resources/packagers/boca/compare.sh +48 -0
  22. rbx/resources/packagers/boca/interactive/c +207 -0
  23. rbx/resources/packagers/boca/interactive/cc +207 -0
  24. rbx/resources/packagers/boca/interactive/cpp +207 -0
  25. rbx/resources/packagers/boca/interactive/java +240 -0
  26. rbx/resources/packagers/boca/interactive/kt +231 -0
  27. rbx/resources/packagers/boca/interactive/py2 +209 -0
  28. rbx/resources/packagers/boca/interactive/py3 +209 -0
  29. rbx/resources/packagers/boca/interactor_compile.sh +45 -0
  30. rbx/resources/packagers/boca/run/bkp +163 -0
  31. rbx/resources/packagers/boca/run/c +19 -19
  32. rbx/resources/packagers/boca/run/cc +19 -19
  33. rbx/resources/packagers/boca/run/cpp +19 -19
  34. rbx/resources/packagers/boca/run/java +51 -51
  35. rbx/resources/packagers/boca/run/kt +30 -30
  36. rbx/resources/packagers/boca/run/py2 +42 -42
  37. rbx/resources/packagers/boca/run/py3 +42 -42
  38. rbx/resources/packagers/moj/scripts/c/compile.sh +19 -0
  39. rbx/resources/packagers/moj/scripts/c/prep.sh +5 -0
  40. rbx/resources/packagers/moj/scripts/c/run.sh +16 -0
  41. rbx/resources/packagers/moj/scripts/compare.sh +32 -6
  42. rbx/resources/packagers/moj/scripts/cpp/compile.sh +19 -0
  43. rbx/resources/packagers/moj/scripts/cpp/prep.sh +5 -0
  44. rbx/resources/packagers/moj/scripts/cpp/run.sh +16 -0
  45. rbx/resources/packagers/moj/scripts/interactor_prep.sh +14 -0
  46. rbx/resources/packagers/moj/scripts/interactor_run.sh +38 -0
  47. rbx/resources/packagers/moj/scripts/java/compile.sh +12 -0
  48. rbx/resources/packagers/moj/scripts/java/prep.sh +8 -0
  49. rbx/resources/packagers/moj/scripts/java/run.sh +17 -0
  50. rbx/resources/packagers/moj/scripts/py2/compile.sh +7 -0
  51. rbx/resources/packagers/moj/scripts/py2/prep.sh +5 -0
  52. rbx/resources/packagers/moj/scripts/py2/run.sh +16 -0
  53. rbx/resources/packagers/moj/scripts/py3/compile.sh +7 -0
  54. rbx/resources/packagers/moj/scripts/py3/prep.sh +5 -0
  55. rbx/resources/packagers/moj/scripts/py3/run.sh +16 -0
  56. {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/METADATA +1 -1
  57. {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/RECORD +60 -33
  58. rbx/resources/packagers/boca/compare +0 -53
  59. {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/LICENSE +0 -0
  60. {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/WHEEL +0 -0
  61. {rbx_cp-0.5.45.dist-info → rbx_cp-0.5.47.dist-info}/entry_points.txt +0 -0
@@ -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(self) -> str:
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
- def name(self) -> str:
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).ask()
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
@@ -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=testcase.inputPath,
71
- outputPath=testcase.outputPath or utils.get_empty_sentinel_path(),
72
- hasOutput=testcase.outputPath is not None,
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
@@ -50,7 +50,6 @@ def _compile_finder(finder: CodeItem) -> str:
50
50
  return digest
51
51
 
52
52
 
53
- @syncer.sync
54
53
  async def run_stress(
55
54
  name: str,
56
55
  timeoutInSeconds: int,
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
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
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
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
189
- error_path = output_path.with_suffix('.err')
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(error_path),
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
- error_path,
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=error_path.absolute(),
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
+ )
@@ -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
- return exitcode == 0
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 ' 'with command: %s',
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 != 0:
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
- self.returncode = subprocess.call(
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 = set()
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
- wait_and_finish(sub_pid, options, start_time, alarm_msg=alarm_msg)
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()