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
@@ -2,11 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  import pathlib
4
4
  from enum import Enum
5
- from typing import List, Literal, Union
5
+ from typing import Annotated, List, Literal, Optional, Union
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field
7
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field
8
8
 
9
9
  from rbx.autoenum import AutoEnum, alias
10
+ from rbx.box.fields import FNameField
11
+ from rbx.box.lang import is_valid_lang_code
12
+
13
+
14
+ def validate_statement_language(lang: str):
15
+ if not is_valid_lang_code(lang) or not lang.islower():
16
+ raise ValueError(
17
+ f'Invalid statement language: {lang}. Language must be a valid lowercase ISO 639-1 code.'
18
+ )
19
+ return lang
20
+
21
+
22
+ StatementLanguage = Annotated[str, AfterValidator(validate_statement_language)]
10
23
 
11
24
 
12
25
  ### Conversion types
@@ -95,13 +108,28 @@ class StatementType(AutoEnum):
95
108
  class Statement(BaseModel):
96
109
  model_config = ConfigDict(extra='forbid')
97
110
 
111
+ name: str = FNameField(description='Name of this statement.')
112
+
113
+ extends: Optional[str] = FNameField(
114
+ default=None,
115
+ description='Name of the statement that this statement extends.',
116
+ )
117
+
118
+ language: StatementLanguage = Field(
119
+ default='en', description='Language code of this statement (ISO 639-1).'
120
+ )
121
+
98
122
  title: str = Field(
99
- description='Name of the problem, as it appears in the statement.'
123
+ default='', description='Name of the problem, as it appears in the statement.'
100
124
  )
101
125
 
102
- path: pathlib.Path = Field(description='Path to the input statement file.')
126
+ path: pathlib.Path = Field(
127
+ default_factory=pathlib.Path, description='Path to the input statement file.'
128
+ )
103
129
 
104
- type: StatementType = Field(description='Type of the input statement file.')
130
+ type: StatementType = Field(
131
+ default=StatementType.rbxTeX, description='Type of the input statement file.'
132
+ )
105
133
 
106
134
  steps: List[ConversionStep] = Field(
107
135
  default=[],
@@ -134,7 +162,3 @@ the statement. Files will be included in the same folder as the statement file,
134
162
  their relativeness. Can be glob pattern as well, such as `imgs/*.png`.
135
163
  """,
136
164
  )
137
-
138
- language: str = Field(
139
- default='en', description='Language this is statement is written in.'
140
- )
rbx/box/testcase_utils.py CHANGED
@@ -160,6 +160,10 @@ def fill_output_for_defined_testcase(testcase: Testcase) -> Testcase:
160
160
  return res
161
161
 
162
162
 
163
+ class TestcaseInteractionParsingError(Exception):
164
+ pass
165
+
166
+
163
167
  def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
164
168
  entries = []
165
169
  with file.open('r') as f:
@@ -167,53 +171,21 @@ def parse_interaction(file: pathlib.Path) -> TestcaseInteraction:
167
171
  interactor_prefix = f.readline().strip()
168
172
  solution_prefix = f.readline().strip()
169
173
  except Exception:
170
- console.console.print(
171
- f'[error]Failed to read interaction file [item]{file}[/item]. Expected the first two lines to be the interactor and solution prefixes.[/error]'
172
- )
173
- raise typer.Exit(1) from None
174
-
175
- # Crop file.
176
- rest = f.read()
177
- start = 0
178
-
179
- def _find_next_prefix(start: int) -> Optional[Tuple[int, int]]:
180
- interactor_idx = rest.find(interactor_prefix, start)
181
- solution_idx = rest.find(solution_prefix, start)
182
- if interactor_idx == -1 and solution_idx == -1:
183
- return None
184
- if interactor_idx == -1:
185
- return (solution_idx, solution_idx + len(solution_prefix))
186
- if solution_idx == -1:
187
- return (interactor_idx, interactor_idx + len(interactor_prefix))
188
- if interactor_idx < solution_idx:
189
- return (interactor_idx, interactor_idx + len(interactor_prefix))
190
- return (solution_idx, solution_idx + len(solution_prefix))
191
-
192
- def _find_next_block() -> Optional[Tuple[int, Tuple[int, int]]]:
193
- prefix = _find_next_prefix(start)
194
- if prefix is None:
195
- return None
196
- prefix_start, prefix_end = prefix
197
- prefix = rest[prefix_start:prefix_end]
198
- pipe = 1 if prefix == solution_prefix else 0
199
-
200
- nxt = _find_next_prefix(prefix_end)
201
- if nxt is None:
202
- return (pipe, (prefix_end, len(rest)))
203
- nxt_start, _ = nxt
204
- return (pipe, (prefix_end, nxt_start))
205
-
206
- # TODO: optimize
207
- blocks = 0
208
- MAX_BLOCKS = 1024
209
- while blocks < MAX_BLOCKS:
210
- block = _find_next_block()
211
- if block is None:
212
- break
213
- pipe, (st, nd) = block
214
- entries.append(TestcaseInteractionEntry(data=rest[st:nd], pipe=pipe))
215
- start = nd
216
- blocks += 1
174
+ raise TestcaseInteractionParsingError(
175
+ f'Failed to read interaction file {file}. Expected the first two lines to be the interactor and solution prefixes.'
176
+ ) from None
177
+
178
+ while line := f.readline().strip():
179
+ if line.startswith(interactor_prefix):
180
+ stripped = line[len(interactor_prefix) :].strip()
181
+ entries.append(TestcaseInteractionEntry(data=stripped, pipe=0))
182
+ elif line.startswith(solution_prefix):
183
+ stripped = line[len(solution_prefix) :].strip()
184
+ entries.append(TestcaseInteractionEntry(data=stripped, pipe=1))
185
+ else:
186
+ raise TestcaseInteractionParsingError(
187
+ f'Invalid line in interaction file {file}. Expected the line to start with the interactor or solution prefix ({interactor_prefix} or {solution_prefix}).'
188
+ ) from None
217
189
 
218
190
  return TestcaseInteraction(
219
191
  prefixes=(interactor_prefix, solution_prefix),
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ import pathlib
2
+
3
+ import typer
4
+
5
+ from rbx import annotations
6
+ from rbx.box.tooling.boca.scrape import scrape_boca
7
+
8
+ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
9
+
10
+
11
+ @app.command('scrape', help='Scrape runs from BOCA.')
12
+ def scrape() -> None:
13
+ scrape_boca(pathlib.Path())
@@ -0,0 +1,34 @@
1
+ import pathlib
2
+ from concurrent.futures import ThreadPoolExecutor
3
+
4
+ from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn
5
+
6
+ from rbx.box.tooling.boca.scraper import BocaRun, BocaScraper
7
+
8
+
9
+ def scrape_boca(into_path: pathlib.Path):
10
+ scraper = BocaScraper()
11
+ scraper.login()
12
+ runs = scraper.list_runs()
13
+
14
+ progress = Progress(
15
+ SpinnerColumn(),
16
+ *Progress.get_default_columns(),
17
+ MofNCompleteColumn(),
18
+ transient=True,
19
+ )
20
+ scrape_task = progress.add_task('Scraping runs...', total=len(runs))
21
+ with progress:
22
+
23
+ def work(run: BocaRun):
24
+ scraper.download_run(
25
+ run.run_number,
26
+ run.site_number,
27
+ pathlib.Path(into_path) / run.problem_shortname,
28
+ name=f'{run.run_number}-{run.site_number}-{run.outcome.short_name().lower()}',
29
+ )
30
+
31
+ progress.update(scrape_task, advance=1)
32
+
33
+ with ThreadPoolExecutor(max_workers=10) as executor:
34
+ executor.map(work, runs)
@@ -6,15 +6,17 @@ import pathlib
6
6
  import re
7
7
  import shutil
8
8
  import typing
9
- from typing import Any, NoReturn, Optional, Tuple
9
+ from typing import Any, List, NoReturn, Optional, Tuple
10
10
 
11
11
  import dateparser
12
12
  import mechanize
13
13
  import typer
14
14
  from bs4 import BeautifulSoup
15
+ from pydantic import BaseModel
15
16
 
16
17
  from rbx import console
17
18
  from rbx.box import naming
19
+ from rbx.grading.steps import Outcome
18
20
 
19
21
  ALERT_REGEX = re.compile(r'\<script[^\>]*\>\s*alert\(\'([^\']+)\'\);?\s*\<\/script\>')
20
22
  UPLOAD_LOG_REGEX = re.compile(r'Problem (\d+) \([^\)]+\) updated')
@@ -32,7 +34,30 @@ def _parse_env_var(var: str, override: Optional[str]) -> str:
32
34
  return value
33
35
 
34
36
 
35
- class BocaUploader:
37
+ def _parse_answer_as_outcome(answer: str) -> Optional[Outcome]:
38
+ answer = answer.lower()
39
+ if 'yes' in answer:
40
+ return Outcome.ACCEPTED
41
+ if 'wrong answer' in answer:
42
+ return Outcome.WRONG_ANSWER
43
+ if 'time limit exceeded' in answer:
44
+ return Outcome.TIME_LIMIT_EXCEEDED
45
+ if 'runtime error' in answer:
46
+ return Outcome.RUNTIME_ERROR
47
+ return None
48
+
49
+
50
+ class BocaRun(BaseModel):
51
+ run_number: int
52
+ site_number: int
53
+ problem_shortname: str
54
+ outcome: Outcome
55
+ time: int
56
+
57
+ user: Optional[str] = None
58
+
59
+
60
+ class BocaScraper:
36
61
  def __init__(
37
62
  self,
38
63
  base_url: Optional[str] = None,
@@ -256,7 +281,49 @@ class BocaUploader:
256
281
  )
257
282
  raise typer.Exit(1)
258
283
 
259
- def download_run(self, run_number: int, site_number: int, into_dir: pathlib.Path):
284
+ def list_runs(self) -> List[BocaRun]:
285
+ _, html = self.open(
286
+ f'{self.base_url}/admin/run.php',
287
+ error_msg='Error while listing runs in BOCA',
288
+ )
289
+
290
+ soup = BeautifulSoup(html, 'html.parser')
291
+ rows = soup.select('form[name="form1"] table tr')
292
+
293
+ runs: List[BocaRun] = []
294
+ for row in rows[1:]:
295
+ cells = row.select('td')
296
+
297
+ run_number = cells[0].text.strip()
298
+ site_number = cells[1].text.strip()
299
+ shortname = cells[4].text.strip()
300
+ answer = cells[-1].text.strip()
301
+ time = int(cells[3].text.strip())
302
+ user = cells[2].text.strip()
303
+
304
+ outcome = _parse_answer_as_outcome(answer)
305
+ if outcome is None:
306
+ continue
307
+ runs.append(
308
+ BocaRun(
309
+ run_number=run_number,
310
+ site_number=site_number,
311
+ problem_shortname=shortname,
312
+ outcome=outcome,
313
+ time=time,
314
+ user=user,
315
+ )
316
+ )
317
+
318
+ return runs
319
+
320
+ def download_run(
321
+ self,
322
+ run_number: int,
323
+ site_number: int,
324
+ into_dir: pathlib.Path,
325
+ name: Optional[str] = None,
326
+ ):
260
327
  url = f'{self.base_url}/admin/runedit.php?runnumber={run_number}&runsitenumber={site_number}'
261
328
  _, html = self.open(
262
329
  url,
@@ -277,7 +344,8 @@ class BocaUploader:
277
344
  if link_col is None:
278
345
  continue
279
346
  href = str(link_col.attrs['href'])
280
- filename = pathlib.Path(link_col.text.strip())
347
+ if filename is None:
348
+ filename = pathlib.Path(link_col.text.strip())
281
349
  break
282
350
 
283
351
  if href is None or filename is None:
@@ -289,16 +357,17 @@ class BocaUploader:
289
357
  tmp_file, _ = self.br.retrieve(link.absolute_url)
290
358
  if tmp_file is None:
291
359
  self.raw_error('Error while downloading run:\nDownloaded file is None.')
292
- final_path = into_dir / filename.with_stem(f'{run_number}-{site_number}')
360
+ filename = filename.with_stem(name or f'{run_number}-{site_number}')
361
+ final_path = into_dir / filename
293
362
  final_path.parent.mkdir(parents=True, exist_ok=True)
294
363
  shutil.move(tmp_file, final_path)
295
364
  return final_path
296
365
 
297
366
 
298
367
  @functools.lru_cache
299
- def get_boca_uploader(
368
+ def get_boca_scraper(
300
369
  base_url: Optional[str] = None,
301
370
  username: Optional[str] = None,
302
371
  password: Optional[str] = None,
303
- ) -> BocaUploader:
304
- return BocaUploader(base_url, username, password)
372
+ ) -> BocaScraper:
373
+ return BocaScraper(base_url, username, password)
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from rbx import annotations
4
+ from rbx.box.tooling.boca import main as boca_main
5
+
6
+ app = typer.Typer(no_args_is_help=True, cls=annotations.AliasGroup)
7
+
8
+ app.add_typer(boca_main.app, name='boca')
@@ -46,7 +46,7 @@ class RunExplorerScreen(Screen):
46
46
  if pkg.type == TaskType.COMMUNICATION:
47
47
  tips.display = True
48
48
  tips.write(
49
- 'This is an interactive problem.\nYou can use the [bold blue]rbx -d run[/bold blue] command to capture the interaction between the processes and see them here.'
49
+ 'This is an interactive problem.\nYou can use the [bold blue]rbx --capture run[/bold blue] command to capture the interaction between the processes and see them here.'
50
50
  )
51
51
  yield tips
52
52
 
@@ -28,13 +28,31 @@ class InteractionBox(RichLogBox):
28
28
  self.auto_scroll = False
29
29
  self.can_focus = False
30
30
 
31
+ def _show_raw_text(self, path: pathlib.Path):
32
+ path_str = str(path.relative_to(pathlib.Path.cwd()))
33
+ self.write(
34
+ rich.text.Text(
35
+ 'Showing raw interaction file because the interaction text is not parseable.\n'
36
+ 'This might usually happen when the processes do not communicate properly.\n',
37
+ style='red',
38
+ )
39
+ )
40
+ self.write(rich.text.Text(path.read_text()))
41
+ self.border_subtitle = f'{path_str} (raw file)'
42
+
31
43
  @work(exclusive=True)
32
44
  async def _load_file(self, path: pathlib.Path):
33
45
  self.clear()
34
46
  path_str = str(path.relative_to(pathlib.Path.cwd()))
35
47
  self.border_subtitle = f'{path_str} (loading...)'
36
48
 
37
- interaction = await asyncio.to_thread(testcase_utils.parse_interaction, path)
49
+ try:
50
+ interaction = await asyncio.to_thread(
51
+ testcase_utils.parse_interaction, path
52
+ )
53
+ except testcase_utils.TestcaseInteractionParsingError:
54
+ self._show_raw_text(path)
55
+ return
38
56
 
39
57
  for entry in interaction.entries:
40
58
  if entry.pipe == 0:
rbx/grading/caching.py CHANGED
@@ -7,10 +7,13 @@ from typing import Any, Dict, List, Optional
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
+ from rbx import console
10
11
  from rbx.grading.judge.digester import digest_cooperatively
11
12
  from rbx.grading.judge.storage import Storage, copyfileobj
12
13
  from rbx.grading.steps import DigestHolder, GradingArtifacts, GradingLogsHolder
13
14
 
15
+ VERBOSE = False
16
+
14
17
 
15
18
  class CacheInput(BaseModel):
16
19
  """
@@ -157,12 +160,21 @@ def _build_cache_input(
157
160
  artifacts.model_copy(deep=True) for artifacts in artifact_list
158
161
  ]
159
162
  for artifacts in cloned_artifact_list:
163
+ # Clear logs from cache input, since they are not
164
+ # part of the cache key.
165
+ artifacts.logs = None
166
+
160
167
  for output in artifacts.outputs:
161
168
  if output.hash:
162
169
  # Cleanup dest field from hash artifacts
163
170
  # since they only their digest value should
164
171
  # be tracked by cache.
165
172
  output.dest = None
173
+
174
+ if output.digest is not None:
175
+ # Cleanup output digest value from cache input,
176
+ # since it is not part of the cache key.
177
+ output.digest.value = None
166
178
  return CacheInput(
167
179
  commands=commands, artifacts=cloned_artifact_list, extra_params=extra_params
168
180
  )
@@ -237,7 +249,11 @@ class DependencyCacheBlock:
237
249
  artifact_list=self.artifact_list,
238
250
  extra_params=self.extra_params,
239
251
  )
252
+ if VERBOSE:
253
+ console.console.log(f'Cache input is: {input}')
240
254
  self._key = _build_cache_key(input)
255
+ if VERBOSE:
256
+ console.console.log(f'Cache key is: {self._key}')
241
257
  found = self.cache.find_in_cache(
242
258
  self.commands, self.artifact_list, self.extra_params, key=self._key
243
259
  )
@@ -349,8 +365,8 @@ class DependencyCache:
349
365
  extra_params: Dict[str, Any],
350
366
  key: Optional[str] = None,
351
367
  ):
352
- input = CacheInput(
353
- commands=commands, artifacts=artifact_list, extra_params=extra_params
368
+ input = _build_cache_input(
369
+ commands=commands, artifact_list=artifact_list, extra_params=extra_params
354
370
  )
355
371
  key = key or _build_cache_key(input)
356
372
 
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import asyncio
2
3
  import collections
3
4
  import dataclasses
4
5
  import io
@@ -15,7 +16,6 @@ from typing import IO, Any, Dict, List, Optional
15
16
 
16
17
  import pydantic
17
18
 
18
- from rbx.grading import processing_context
19
19
  from rbx.grading.judge import cacher, storage
20
20
 
21
21
  logger = logging.getLogger(__name__)
@@ -23,6 +23,21 @@ logger = logging.getLogger(__name__)
23
23
  MERGE_STDERR = pathlib.PosixPath('/dev/stdout')
24
24
 
25
25
 
26
+ # Thread-safe version of asyncio.Event.
27
+ class Event_ts(asyncio.Event):
28
+ def get_loop(self):
29
+ if self._loop is None:
30
+ return asyncio.get_event_loop()
31
+ else:
32
+ return self._loop
33
+
34
+ def set(self):
35
+ self.get_loop().call_soon_threadsafe(super().set)
36
+
37
+ def clear(self):
38
+ self.get_loop().call_soon_threadsafe(super().clear)
39
+
40
+
26
41
  def wait_without_std(
27
42
  procs: List[subprocess.Popen], actually_pipe_to_stdout: bool = False
28
43
  ) -> List[int]:
@@ -116,6 +131,7 @@ class SandboxParams(pydantic.BaseModel):
116
131
  wallclock_timeout: Optional[int] = None # ms
117
132
  extra_timeout: Optional[int] = None # ms
118
133
  reverse_io: bool = False
134
+ pgid: Optional[int] = None
119
135
 
120
136
  # For timeit
121
137
  timeit_dups: Dict[str, List[pathlib.Path]] = dataclasses.field(
@@ -229,6 +245,7 @@ class SandboxBase(abc.ABC):
229
245
 
230
246
  self.params = params or SandboxParams()
231
247
  self.pid = None
248
+ self._pid_event = Event_ts()
232
249
 
233
250
  # Set common environment variables.
234
251
  # Specifically needed by Python, that searches the home for
@@ -329,18 +346,35 @@ class SandboxBase(abc.ABC):
329
346
  pass
330
347
 
331
348
  def set_pid(self, pid: int):
332
- processing_context.add_to_processing_context(pid)
349
+ """Set the PID of the sandboxed process.
350
+
351
+ pid (int): the PID of the sandboxed process.
352
+
353
+ """
333
354
  self.pid = pid
355
+ self._pid_event.set()
334
356
 
335
- def get_pid(self) -> Optional[int]:
357
+ async def get_pid(self) -> int:
336
358
  """Return the PID of the sandboxed process.
337
359
 
338
- return (int|None): the PID of the sandboxed process, or None if
339
- the sandboxed process is not running.
360
+ Blocks until the PID is set.
361
+
362
+ return (int): the PID of the sandboxed process.
340
363
 
341
364
  """
365
+ await self._pid_event.wait()
366
+ assert self.pid is not None
342
367
  return self.pid
343
368
 
369
+ def clear_pid(self):
370
+ """Clear the PID of the sandboxed process."""
371
+ self._pid_event.clear()
372
+ self.pid = None
373
+
374
+ def use_pgid(self) -> bool:
375
+ """Whether the sandbox supports process groups."""
376
+ return False
377
+
344
378
  @abc.abstractmethod
345
379
  def get_detailed_logs(self) -> str:
346
380
  """Return the detailed logs of the sandbox.
@@ -706,6 +740,15 @@ class SandboxBase(abc.ABC):
706
740
  """
707
741
  pass
708
742
 
743
+ def reset(self):
744
+ """Reset the sandbox.
745
+
746
+ To be called at the beginning of the execution.
747
+
748
+ """
749
+ self.cleanup(delete=True)
750
+ self.initialize()
751
+
709
752
  @abc.abstractmethod
710
753
  def cleanup(self, delete: bool = False):
711
754
  """Cleanup the sandbox.
@@ -599,6 +599,7 @@ class IsolateSandbox(SandboxBase):
599
599
  return (bool|Popen): return True if the sandbox didn't report
600
600
  errors (caused by the sandbox itself), False otherwise.
601
601
  """
602
+ self.clear_pid()
602
603
  popen = self._popen(
603
604
  command,
604
605
  stdin=subprocess.PIPE,
@@ -51,23 +51,22 @@ class StupidSandbox(SandboxBase):
51
51
  SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
52
52
 
53
53
  # Make box directory
54
+ self.initialize()
55
+
56
+ def initialize(self):
54
57
  self._path = pathlib.Path(
55
58
  tempfile.mkdtemp(dir=str(self.temp_dir), prefix='rbx-%s-' % (self.name))
56
59
  )
57
- self.initialize()
58
-
59
60
  self.exec_num = -1
60
61
  self.log = None
61
62
  self.returncode = None
63
+ self._path.mkdir(parents=True, exist_ok=True)
62
64
 
63
65
  logger.debug("Sandbox in `%s' created, using stupid box.", self._path)
64
66
 
65
67
  # Box parameters
66
68
  self.chdir = self._path
67
69
 
68
- def initialize(self):
69
- self._path.mkdir(parents=True, exist_ok=True)
70
-
71
70
  def get_timeit_executable(self) -> pathlib.Path:
72
71
  with importlib.resources.as_file(
73
72
  importlib.resources.files('rbx')
@@ -94,6 +93,8 @@ class StupidSandbox(SandboxBase):
94
93
  args.append(f'-f{self.params.fsize}')
95
94
  if self.chdir:
96
95
  args.append(f'-c{self.chdir}')
96
+ if self.use_pgid() and self.params.pgid is not None:
97
+ args.append(f'-g{self.params.pgid}')
97
98
 
98
99
  file_args = []
99
100
  if self.params.stdin_file:
@@ -148,6 +149,9 @@ class StupidSandbox(SandboxBase):
148
149
  def use_soft_timeout(self) -> bool:
149
150
  return True
150
151
 
152
+ def use_pgid(self) -> bool:
153
+ return True
154
+
151
155
  def get_memory_used(self) -> Optional[int]:
152
156
  """Return the memory used by the sandbox.
153
157
 
@@ -305,6 +309,8 @@ class StupidSandbox(SandboxBase):
305
309
  + self.get_timeit_args()
306
310
  + command
307
311
  )
312
+
313
+ self.clear_pid()
308
314
  with subprocess.Popen(
309
315
  real_command,
310
316
  stdin=subprocess.PIPE,