rbx.cp 0.5.72__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. rbx/annotations.py +21 -1
  2. rbx/box/cli.py +24 -8
  3. rbx/box/code.py +140 -3
  4. rbx/box/contest/build_contest_statements.py +44 -34
  5. rbx/box/contest/contest_utils.py +25 -0
  6. rbx/box/contest/main.py +24 -0
  7. rbx/box/contest/schema.py +52 -8
  8. rbx/box/contest/statements.py +53 -25
  9. rbx/box/download.py +19 -1
  10. rbx/box/fields.py +35 -0
  11. rbx/box/lang.py +27 -0
  12. rbx/box/package.py +1 -1
  13. rbx/box/packaging/boca/packager.py +48 -5
  14. rbx/box/packaging/contest_main.py +13 -0
  15. rbx/box/packaging/main.py +13 -2
  16. rbx/box/packaging/packager.py +4 -4
  17. rbx/box/packaging/pkg/packager.py +142 -0
  18. rbx/box/packaging/polygon/packager.py +2 -24
  19. rbx/box/packaging/polygon/upload.py +35 -17
  20. rbx/box/remote.py +2 -2
  21. rbx/box/schema.py +68 -18
  22. rbx/box/solutions.py +6 -1
  23. rbx/box/statements/build_statements.py +44 -27
  24. rbx/box/statements/builders.py +18 -10
  25. rbx/box/statements/expander.py +49 -0
  26. rbx/box/statements/latex_jinja.py +61 -4
  27. rbx/box/statements/schema.py +33 -9
  28. rbx/box/testcase_utils.py +19 -47
  29. rbx/box/tooling/__init__.py +0 -0
  30. rbx/box/tooling/boca/__init__.py +0 -0
  31. rbx/box/tooling/boca/main.py +13 -0
  32. rbx/box/tooling/boca/scrape.py +34 -0
  33. rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
  34. rbx/box/tooling/main.py +8 -0
  35. rbx/box/ui/screens/run_explorer.py +1 -1
  36. rbx/box/ui/widgets/interaction_box.py +19 -1
  37. rbx/grading/caching.py +18 -2
  38. rbx/grading/judge/sandbox.py +48 -5
  39. rbx/grading/judge/sandboxes/isolate.py +1 -0
  40. rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
  41. rbx/grading/judge/sandboxes/timeit.py +36 -15
  42. rbx/grading/processing_context.py +62 -78
  43. rbx/grading/steps.py +91 -40
  44. rbx/resources/packagers/boca/checker.sh +4 -1
  45. rbx/resources/packagers/boca/compile/c +2 -6
  46. rbx/resources/packagers/boca/compile/cc +2 -6
  47. rbx/resources/packagers/boca/compile/cpp +2 -6
  48. rbx/resources/packagers/boca/compile/java +1 -6
  49. rbx/resources/packagers/boca/compile/kt +24 -28
  50. rbx/resources/packagers/boca/compile/py2 +2 -6
  51. rbx/resources/packagers/boca/compile/py3 +2 -6
  52. rbx/resources/packagers/boca/interactive/c +15 -62
  53. rbx/resources/packagers/boca/interactive/cc +15 -62
  54. rbx/resources/packagers/boca/interactive/cpp +15 -61
  55. rbx/resources/packagers/boca/interactive/java +15 -67
  56. rbx/resources/packagers/boca/interactive/kt +15 -67
  57. rbx/resources/packagers/boca/interactive/py2 +15 -67
  58. rbx/resources/packagers/boca/interactive/py3 +15 -65
  59. rbx/resources/packagers/boca/interactor_compile.sh +5 -2
  60. rbx/resources/packagers/boca/interactor_run.sh +174 -0
  61. rbx/resources/packagers/boca/safeexec.c +530 -0
  62. rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
  63. rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
  64. rbx/resources/presets/default/problem/problem.rbx.yml +27 -26
  65. rbx/resources/templates/rbx.h +2 -3
  66. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
  67. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +70 -59
  68. rbx/resources/packagers/boca/compile/pas +0 -172
  69. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
  70. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
  71. {rbx_cp-0.5.72.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,7 @@ import resource
5
5
  import signal
6
6
  import stat
7
7
  import sys
8
+ import threading
8
9
  from time import monotonic
9
10
  from typing import Any, Dict, List, Optional, Set, Union
10
11
 
@@ -25,6 +26,7 @@ class Options:
25
26
  file_duplicates: Dict[int, List[str]] = dataclasses.field(default_factory=dict)
26
27
  prefixed: Set[str] = dataclasses.field(default_factory=set)
27
28
  prefix: str = ''
29
+ process_group: Optional[int] = None
28
30
 
29
31
 
30
32
  def exit_with(code: int):
@@ -91,17 +93,16 @@ def create_tee(files, mode, buffer_size=4096, prefix=''):
91
93
  # Close parent's end of the pipe
92
94
  os.close(pipe_write)
93
95
 
94
- bytes = os.read(pipe_read, buffer_size)
95
- while bytes:
96
+ new = True
97
+ while bytes := os.read(pipe_read, 1):
96
98
  for tee in tee_list:
97
- if tee.prefix:
99
+ if tee.prefix and new:
98
100
  tee.file.write(tee.prefix)
99
101
  tee.file.write(bytes)
100
102
  tee.file.flush()
103
+ new = bytes == b'\n'
101
104
  # TODO maybe add in fsync() here if the fileno() method
102
105
  # exists on file
103
-
104
- bytes = os.read(pipe_read, buffer_size)
105
106
  except Exception:
106
107
  pass
107
108
  finally:
@@ -150,6 +151,8 @@ def parse_opts() -> Options:
150
151
  options.fs_limit = int(opt[2:])
151
152
  elif opt.startswith('-P'):
152
153
  options.prefix = opt[2:]
154
+ elif opt.startswith('-g'):
155
+ options.process_group = int(opt[2:])
153
156
  else:
154
157
  raise Exception(f'Invalid option {opt}')
155
158
  num_opts += 1
@@ -270,6 +273,7 @@ def wait_and_finish(
270
273
  entries.append(f'time-wall: {wall_time:.3f}')
271
274
  entries.append(f'mem: {memory_used}')
272
275
  entries.append(f'file: {file_sizes}')
276
+ entries.append(f'pid: {pid}')
273
277
 
274
278
  output_file = pathlib.Path(sys.argv[1])
275
279
  output_file.parent.mkdir(parents=True, exist_ok=True)
@@ -279,6 +283,9 @@ def wait_and_finish(
279
283
  def main():
280
284
  options = parse_opts()
281
285
 
286
+ if options.process_group is not None:
287
+ os.setpgid(0, options.process_group)
288
+
282
289
  start_time = monotonic()
283
290
  sub_pid = os.fork()
284
291
  if sub_pid == 0:
@@ -292,13 +299,21 @@ def main():
292
299
  alarm_msg: List[Optional[str]] = [None]
293
300
  status_holder: Set[str] = set()
294
301
 
295
- def handle_alarm(*args, **kwargs):
302
+ stop_wall_handler = threading.Event()
303
+ stop_alarm_handler = threading.Event()
304
+
305
+ def handle_wall():
306
+ if stop_wall_handler.wait(options.wall_time_limit):
307
+ return
308
+ stop_alarm_handler.set()
296
309
  nonlocal alarm_msg
297
- wall_time = monotonic() - start_time
298
- if options.wall_time_limit is not None and wall_time > options.wall_time_limit:
299
- alarm_msg[0] = 'wall timelimit'
300
- os.kill(sub_pid, 9)
310
+ alarm_msg[0] = 'wall timelimit'
311
+ os.kill(sub_pid, 9)
312
+
313
+ def handle_alarm():
314
+ if stop_alarm_handler.wait(0.3):
301
315
  return
316
+ nonlocal alarm_msg
302
317
  ru = resource.getrusage(resource.RUSAGE_CHILDREN)
303
318
  if options.time_limit is not None:
304
319
  cpu_time = get_cpu_time(ru)
@@ -313,20 +328,26 @@ def main():
313
328
  os.kill(sub_pid, 9)
314
329
  return
315
330
 
316
- signal.setitimer(signal.ITIMER_REAL, 0.3)
331
+ stop_alarm_handler.clear()
332
+ handle_alarm()
333
+
334
+ alarm_handler = threading.Thread(target=handle_alarm, daemon=True)
335
+ wall_handler = threading.Thread(target=handle_wall, daemon=True)
336
+ alarm_handler.start()
337
+ wall_handler.start()
317
338
 
318
339
  def handle_sub_term(*args, **kwargs):
319
340
  nonlocal status_holder
320
341
  status_holder.add('TE')
321
342
  os.kill(sub_pid, 9)
322
343
 
323
- signal.setitimer(signal.ITIMER_REAL, 0.3)
324
- signal.signal(signal.SIGALRM, handle_alarm)
325
344
  signal.signal(signal.SIGTERM, handle_sub_term)
326
345
 
327
346
  wait_and_finish(sub_pid, options, start_time, status_holder, alarm_msg=alarm_msg)
328
- # Cancel alarm before exiting to avoid surprises.
329
- signal.setitimer(signal.ITIMER_REAL, 0)
347
+
348
+ # Process finished, stop the handlers.
349
+ stop_wall_handler.set()
350
+ stop_alarm_handler.set()
330
351
 
331
352
  # Exit gracefully.
332
353
  sys.exit(0)
@@ -1,87 +1,71 @@
1
- import asyncio
2
1
  import contextlib
3
2
  import os
4
3
  import signal
5
- import threading
6
- from typing import Optional, Set
4
+ import subprocess
5
+ from typing import List, Optional
7
6
 
8
- _processing_context_pids: Optional[Set[int]] = None
9
- _terminate_all_on_error = False
10
- _lock = threading.Lock()
11
-
12
- # Creating a processing context is not thread-safe, but adding to it is.
7
+ from rbx.grading.judge.sandbox import SandboxBase
13
8
 
14
9
 
15
10
  @contextlib.contextmanager
16
- def new_processing_context(terminate_all_on_error: bool = False):
17
- global _processing_context_pids, _terminate_all_on_error
18
- with _lock:
19
- old_processing_context_pids = _processing_context_pids
20
- _old_terminate_all_on_error = _terminate_all_on_error
21
- _processing_context_pids = set()
22
- _terminate_all_on_error = terminate_all_on_error
11
+ def new_process_group():
12
+ p = subprocess.Popen(['/bin/bash', '-c', 'exec sleep infinity'])
23
13
  try:
24
- yield
14
+ yield p.pid
25
15
  finally:
26
- with _lock:
27
- _processing_context_pids = old_processing_context_pids
28
- _terminate_all_on_error = _old_terminate_all_on_error
29
-
30
-
31
- def get_processing_context() -> Set[int]:
32
- with _lock:
33
- return _processing_context_pids or set()
34
-
35
-
36
- def add_to_processing_context(pid: int):
37
- global _processing_context_pids
38
- with _lock:
39
- if _processing_context_pids is None:
40
- return
41
- _processing_context_pids.add(pid)
42
-
43
-
44
- def terminate_all_processes_in_context(clear: bool = True):
45
- global _processing_context_pids
46
- with _lock:
47
- if _processing_context_pids is None:
48
- return
49
- for pid in _processing_context_pids:
50
- try:
51
- os.kill(pid, signal.SIGTERM)
52
- except OSError:
53
- pass
54
- if clear:
55
- _processing_context_pids.clear()
56
-
57
-
58
- async def wait_all_processes_in_context(wait_for: int):
59
- global _processing_context_pids, _terminate_all_on_error
60
- wait_pids = set()
61
- while len(get_processing_context()) < wait_for:
62
- await asyncio.sleep(0.01)
63
-
64
- with _lock:
65
- if _processing_context_pids is None:
66
- return
67
- wait_pids.update(_processing_context_pids)
68
-
69
- wait_lock = threading.Lock()
70
- finished_pids = []
71
-
72
- def process(pid: int, returncode: int):
73
- with wait_lock:
74
- finished_pids.append(pid)
75
- if returncode != 0 and _terminate_all_on_error:
76
- terminate_all_processes_in_context()
77
-
78
- def wait_all_processes():
79
- while len(finished_pids) < len(wait_pids):
80
- try:
81
- pid, status = os.wait()
82
- except ChildProcessError:
83
- return
84
- if pid in wait_pids:
85
- process(pid, os.waitstatus_to_exitcode(status))
86
-
87
- await asyncio.to_thread(wait_all_processes)
16
+ p.terminate()
17
+ p.wait()
18
+
19
+
20
+ def should_use_group(sandboxes: List[SandboxBase]) -> bool:
21
+ if not sandboxes:
22
+ return False
23
+ uses_pgid = all(sandbox.use_pgid() for sandbox in sandboxes)
24
+ all_pgids = set(
25
+ sandbox.params.pgid for sandbox in sandboxes if sandbox.params.pgid is not None
26
+ )
27
+ return uses_pgid and len(all_pgids) == 1
28
+
29
+
30
+ async def _fetch_pids(sandboxes: List[SandboxBase]) -> List[int]:
31
+ return [await sandbox.get_pid() for sandbox in sandboxes]
32
+
33
+
34
+ def _find_sandbox_idx(pids: List[int], pid: int) -> Optional[int]:
35
+ try:
36
+ return pids.index(pid)
37
+ except ValueError:
38
+ return None
39
+
40
+
41
+ async def _wait_for_group(sandboxes: List[SandboxBase]) -> List[int]:
42
+ pgid = [
43
+ sandbox.params.pgid for sandbox in sandboxes if sandbox.params.pgid is not None
44
+ ][0]
45
+ assert pgid is not None
46
+
47
+ sandbox_pids = await _fetch_pids(sandboxes)
48
+
49
+ finished = []
50
+ while len(finished) < len(sandboxes):
51
+ try:
52
+ pid, status = os.waitpid(-pgid, 0)
53
+ except ChildProcessError:
54
+ break
55
+
56
+ if os.waitstatus_to_exitcode(status) != 0:
57
+ os.kill(pgid, signal.SIGKILL)
58
+
59
+ sandbox_idx = _find_sandbox_idx(sandbox_pids, pid)
60
+ if sandbox_idx is not None:
61
+ finished.append(sandbox_idx)
62
+ continue
63
+
64
+ return finished
65
+
66
+
67
+ async def wait_all(sandboxes: List[SandboxBase]):
68
+ if not should_use_group(sandboxes):
69
+ raise RuntimeError('Sandboxes are not using a process group')
70
+
71
+ await _wait_for_group(sandboxes)
rbx/grading/steps.py CHANGED
@@ -10,7 +10,6 @@ import shutil
10
10
  import subprocess
11
11
  import sys
12
12
  import tempfile
13
- import typing
14
13
  from enum import Enum
15
14
  from typing import IO, Any, Dict, Iterable, List, Optional, Tuple, Union
16
15
 
@@ -53,6 +52,27 @@ class Outcome(Enum):
53
52
  Outcome.IDLENESS_LIMIT_EXCEEDED,
54
53
  ]
55
54
 
55
+ def short_name(self) -> str:
56
+ if self == Outcome.ACCEPTED:
57
+ return 'AC'
58
+ if self == Outcome.WRONG_ANSWER:
59
+ return 'WA'
60
+ if self == Outcome.TIME_LIMIT_EXCEEDED:
61
+ return 'TLE'
62
+ if self == Outcome.IDLENESS_LIMIT_EXCEEDED:
63
+ return 'ILE'
64
+ if self == Outcome.MEMORY_LIMIT_EXCEEDED:
65
+ return 'MLE'
66
+ if self == Outcome.RUNTIME_ERROR:
67
+ return 'RTE'
68
+ if self == Outcome.OUTPUT_LIMIT_EXCEEDED:
69
+ return 'OLE'
70
+ if self == Outcome.JUDGE_FAILED:
71
+ return 'FL'
72
+ if self == Outcome.INTERNAL_ERROR:
73
+ return 'IE'
74
+ return 'XX'
75
+
56
76
 
57
77
  class DigestHolder(BaseModel):
58
78
  value: Optional[str] = None
@@ -372,12 +392,12 @@ def _is_c_command(exe_command: str) -> bool:
372
392
  return 'gcc' in exe_command or 'clang' in exe_command
373
393
 
374
394
 
375
- def _is_cpp_command(exe_command: str) -> bool:
395
+ def is_cpp_command(exe_command: str) -> bool:
376
396
  return 'g++' in exe_command or 'clang++' in exe_command
377
397
 
378
398
 
379
399
  def is_cxx_command(exe_command: str) -> bool:
380
- return _is_cpp_command(exe_command) or _is_c_command(exe_command)
400
+ return is_cpp_command(exe_command) or _is_c_command(exe_command)
381
401
 
382
402
 
383
403
  def is_cxx_sanitizer_command(command: str) -> bool:
@@ -413,7 +433,8 @@ def _complain_about_clang() -> None:
413
433
  )
414
434
 
415
435
 
416
- def _get_cxx_version_output(command: str) -> Optional[str]:
436
+ @functools.cache
437
+ def _get_cxx_version_output(command: str, extra_flags: str = '') -> Optional[str]:
417
438
  cmds = shlex.split(command)
418
439
  if not cmds:
419
440
  return None
@@ -421,8 +442,8 @@ def _get_cxx_version_output(command: str) -> Optional[str]:
421
442
  if not is_cxx_command(exe):
422
443
  return None
423
444
 
424
- exe = cmds[0]
425
- output = subprocess.run([exe, '-v'], capture_output=True)
445
+ extra = shlex.split(extra_flags)
446
+ output = subprocess.run([exe, '-v', *extra], capture_output=True, input='')
426
447
  if output.returncode != 0:
427
448
  console.print('[error]Failed to get C/C++ compiler version.[/error]')
428
449
  return None
@@ -430,6 +451,8 @@ def _get_cxx_version_output(command: str) -> Optional[str]:
430
451
 
431
452
 
432
453
  def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]:
454
+ if not is_cpp_command(get_exe_from_command(command)):
455
+ return None
433
456
  version_output = _get_cxx_version_output(command)
434
457
  if version_output is None:
435
458
  return None
@@ -446,11 +469,44 @@ def _maybe_get_bits_stdcpp_for_clang(command: str) -> Optional[GradingFileInput]
446
469
  return GradingFileInput(src=bits, dest=pathlib.Path('bits/stdc++.h'))
447
470
 
448
471
 
449
- def _maybe_get_bits_stdcpp_for_commands(
472
+ def _find_system_paths_in_version_output(version_output: str) -> List[pathlib.Path]:
473
+ res = []
474
+ start = False
475
+ for line in version_output.splitlines():
476
+ if line.startswith('#include <...> search starts here:'):
477
+ start = True
478
+ continue
479
+ if not start:
480
+ continue
481
+ if not line.startswith(' '):
482
+ break
483
+ res.append(pathlib.Path(line.strip()))
484
+ return res
485
+
486
+
487
+ def _get_system_bits_stdcpp(command: str) -> Optional[GradingFileInput]:
488
+ if not is_cpp_command(get_exe_from_command(command)):
489
+ return None
490
+ version_output = _get_cxx_version_output(command, '-xc++ -E -')
491
+ if version_output is None:
492
+ return None
493
+ for path in _find_system_paths_in_version_output(version_output):
494
+ bits_candidate = path / 'bits' / 'stdc++.h'
495
+ if not bits_candidate.is_file():
496
+ continue
497
+ return GradingFileInput(
498
+ src=bits_candidate.resolve().absolute(), dest=pathlib.Path('bits/stdc++.h')
499
+ )
500
+ return None
501
+
502
+
503
+ def maybe_get_bits_stdcpp_for_commands(
450
504
  commands: List[str],
451
505
  ) -> Optional[GradingFileInput]:
452
506
  for command in commands:
453
- res = _maybe_get_bits_stdcpp_for_clang(command)
507
+ res = _get_system_bits_stdcpp(command) or _maybe_get_bits_stdcpp_for_clang(
508
+ command
509
+ )
454
510
  if res is not None:
455
511
  return res
456
512
  return None
@@ -560,10 +616,9 @@ def compile(
560
616
  sandbox: SandboxBase,
561
617
  artifacts: GradingArtifacts,
562
618
  ) -> bool:
619
+ sandbox.reset()
620
+
563
621
  commands = _try_following_alias_for_commands(commands)
564
- bits_artifact = _maybe_get_bits_stdcpp_for_commands(commands)
565
- if bits_artifact is not None:
566
- _process_input_artifacts(GradingArtifacts(inputs=[bits_artifact]), sandbox)
567
622
  _process_input_artifacts(artifacts, sandbox)
568
623
 
569
624
  if not commands:
@@ -571,7 +626,9 @@ def compile(
571
626
  return True
572
627
 
573
628
  logs: List[PreprocessLog] = []
574
- sandbox.set_params(params)
629
+ sandbox.set_params(
630
+ params.model_copy(deep=True)
631
+ ) # Copy to allow further modification.
575
632
 
576
633
  for i, command in enumerate(commands):
577
634
  _maybe_complain_about_sanitization(command)
@@ -584,10 +641,6 @@ def compile(
584
641
  if is_java_like_command(get_exe_from_command(command)):
585
642
  sandbox.params.address_space = None
586
643
 
587
- if bits_artifact is not None and _is_cpp_command(cmd[0]):
588
- # Include from sandbox directory to import bits/stdc++.h.
589
- cmd.append('-I.')
590
-
591
644
  if not sandbox.execute_without_std(cmd):
592
645
  console.print(
593
646
  '[error]Sandbox crashed while processing command:[/error]',
@@ -642,12 +695,15 @@ async def run(
642
695
  sandbox: SandboxBase,
643
696
  artifacts: GradingArtifacts,
644
697
  metadata: Optional[RunLogMetadata] = None,
645
- kill_on_processing_context_exit: bool = False,
646
698
  ) -> Optional[RunLog]:
699
+ sandbox.reset()
700
+
647
701
  _process_input_artifacts(artifacts, sandbox)
648
702
  _process_fifos(artifacts, sandbox)
649
703
  cmd = _split_and_expand(command, sandbox)
650
- sandbox.set_params(params)
704
+ sandbox.set_params(
705
+ params.model_copy(deep=True)
706
+ ) # Copy to allow further modification.
651
707
 
652
708
  # Remove memory constraints for Java.
653
709
  if is_java_like_command(get_exe_from_command(command)):
@@ -662,9 +718,6 @@ async def run(
662
718
  )
663
719
  return None
664
720
 
665
- if sandbox.get_exit_code() != 0 and kill_on_processing_context_exit:
666
- processing_context.terminate_all_processes_in_context(clear=False)
667
-
668
721
  if not _process_output_artifacts(artifacts, sandbox):
669
722
  return None
670
723
 
@@ -706,27 +759,25 @@ async def run_coordinated(
706
759
  interactor: CoordinatedRunParams,
707
760
  solution: CoordinatedRunParams,
708
761
  ) -> Tuple[Optional[RunLog], Optional[RunLog]]:
709
- with processing_context.new_processing_context(terminate_all_on_error=True):
710
- # Schedule both runs to execute immediately.
711
- runs = tuple(
712
- asyncio.create_task(
713
- run(
714
- params.command,
715
- params.params,
716
- params.sandbox,
717
- params.artifacts,
718
- params.metadata,
719
- kill_on_processing_context_exit=True,
720
- )
762
+ def run_one(params: CoordinatedRunParams) -> asyncio.Task[Optional[RunLog]]:
763
+ return asyncio.create_task(
764
+ run(
765
+ params.command,
766
+ params.params,
767
+ params.sandbox,
768
+ params.artifacts,
769
+ params.metadata,
721
770
  )
722
- for params in [interactor, solution]
723
- )
724
- await processing_context.wait_all_processes_in_context(wait_for=2)
725
- logs = typing.cast(
726
- Tuple[Optional[RunLog], Optional[RunLog]],
727
- tuple(await asyncio.gather(*runs)),
728
771
  )
729
- return logs
772
+
773
+ # Use interactor PID as the process group id.
774
+ interactor_task = run_one(interactor)
775
+ solution.sandbox.params.pgid = await interactor.sandbox.get_pid()
776
+ solution_task = run_one(solution)
777
+
778
+ await processing_context.wait_all([interactor.sandbox, solution.sandbox])
779
+
780
+ return await asyncio.gather(interactor_task, solution_task)
730
781
 
731
782
 
732
783
  def _normalize_checked_words(s: str) -> Tuple[str, ...]:
@@ -1,4 +1,7 @@
1
1
  ### START OF CHECKER COMPILATION
2
+ CACHE_DIR="/tmp/boca-cache"
3
+ mkdir -p $CACHE_DIR
4
+
2
5
  CDIR=$(pwd)
3
6
  CHECKER_PATH="checker.cpp"
4
7
  CHECKER_OUT="checker.exe"
@@ -27,7 +30,7 @@ printf "%s" "${RbxHeaderContent}" >rbx.h
27
30
  printf "%s" "${CheckerContent}" >$CHECKER_PATH
28
31
 
29
32
  checker_hash=($(cat $CHECKER_PATH rbx.h testlib.h | md5sum))
30
- checker_cache="/tmp/boca-chk-${checker_hash}"
33
+ checker_cache="$CACHE_DIR/checker-${checker_hash}"
31
34
 
32
35
  echo "Polygon checker hash: $checker_hash"
33
36
  echo "Copying polygon checker to $CDIR/$CHECKER_OUT"
@@ -92,12 +92,8 @@ if [ "$4" != "" ]; then
92
92
  fi
93
93
 
94
94
  # setting up the timelimit according to the problem
95
- if [ "$3" == "" ]; then
96
- time=5
97
- else
98
- time=$3
99
- fi
100
- let "ttime = $time + 30"
95
+ time=15
96
+ let "ttime = $time * 2"
101
97
 
102
98
  if [ "$2" == "" ]; then
103
99
  exe=run.exe
@@ -93,12 +93,8 @@ if [ "$4" != "" ]; then
93
93
  fi
94
94
 
95
95
  # setting up the timelimit according to the problem
96
- if [ "$3" == "" ]; then
97
- time=5
98
- else
99
- time=$3
100
- fi
101
- let "ttime = $time + 30"
96
+ time=15
97
+ let "ttime = $time * 2"
102
98
 
103
99
  if [ "$2" == "" ]; then
104
100
  exe=run.exe
@@ -92,12 +92,8 @@ if [ "$4" != "" ]; then
92
92
  fi
93
93
 
94
94
  # setting up the timelimit according to the problem
95
- if [ "$3" == "" ]; then
96
- time=5
97
- else
98
- time=$3
99
- fi
100
- let "ttime = $time + 30"
95
+ time=15
96
+ let "ttime = $time * 2"
101
97
 
102
98
  if [ "$2" == "" ]; then
103
99
  exe=run.exe
@@ -88,13 +88,8 @@ if [ ! -x $sf ]; then
88
88
  fi
89
89
 
90
90
  # setting up the timelimit according to the problem
91
- if [ "$3" == "" ]; then
92
- time=5
93
- else
94
- time=$3
95
- fi
96
- let "ttime = $time + 30"
97
91
  time=30
92
+ ttime=30
98
93
 
99
94
  maxm=512
100
95
  if [ "$4" != "" -a "$4" -gt "512" ]; then