rbx.cp 0.5.73__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.
- rbx/annotations.py +21 -1
- rbx/box/cli.py +10 -1
- rbx/box/code.py +140 -3
- rbx/box/contest/build_contest_statements.py +44 -34
- rbx/box/contest/schema.py +52 -8
- rbx/box/contest/statements.py +53 -25
- rbx/box/fields.py +35 -0
- rbx/box/lang.py +27 -0
- rbx/box/package.py +1 -1
- rbx/box/packaging/boca/packager.py +48 -5
- rbx/box/packaging/contest_main.py +13 -0
- rbx/box/packaging/main.py +13 -2
- rbx/box/packaging/packager.py +4 -4
- rbx/box/packaging/pkg/packager.py +142 -0
- rbx/box/packaging/polygon/packager.py +2 -24
- rbx/box/packaging/polygon/upload.py +35 -17
- rbx/box/remote.py +2 -2
- rbx/box/schema.py +68 -18
- rbx/box/solutions.py +6 -1
- rbx/box/statements/build_statements.py +44 -27
- rbx/box/statements/builders.py +18 -10
- rbx/box/statements/expander.py +49 -0
- rbx/box/statements/latex_jinja.py +61 -4
- rbx/box/statements/schema.py +33 -9
- rbx/box/testcase_utils.py +19 -47
- rbx/box/tooling/__init__.py +0 -0
- rbx/box/tooling/boca/__init__.py +0 -0
- rbx/box/tooling/boca/main.py +13 -0
- rbx/box/tooling/boca/scrape.py +34 -0
- rbx/box/{packaging/boca/upload.py → tooling/boca/scraper.py} +77 -8
- rbx/box/tooling/main.py +8 -0
- rbx/box/ui/widgets/interaction_box.py +19 -1
- rbx/grading/caching.py +18 -2
- rbx/grading/judge/sandbox.py +48 -5
- rbx/grading/judge/sandboxes/isolate.py +1 -0
- rbx/grading/judge/sandboxes/stupid_sandbox.py +11 -5
- rbx/grading/judge/sandboxes/timeit.py +36 -15
- rbx/grading/processing_context.py +62 -78
- rbx/grading/steps.py +91 -40
- rbx/resources/packagers/boca/checker.sh +4 -1
- rbx/resources/packagers/boca/compile/c +2 -6
- rbx/resources/packagers/boca/compile/cc +2 -6
- rbx/resources/packagers/boca/compile/cpp +2 -6
- rbx/resources/packagers/boca/compile/java +1 -6
- rbx/resources/packagers/boca/compile/kt +24 -28
- rbx/resources/packagers/boca/compile/py2 +2 -6
- rbx/resources/packagers/boca/compile/py3 +2 -6
- rbx/resources/packagers/boca/interactive/c +15 -83
- rbx/resources/packagers/boca/interactive/cc +15 -83
- rbx/resources/packagers/boca/interactive/cpp +15 -83
- rbx/resources/packagers/boca/interactive/java +15 -88
- rbx/resources/packagers/boca/interactive/kt +15 -88
- rbx/resources/packagers/boca/interactive/py2 +15 -88
- rbx/resources/packagers/boca/interactive/py3 +15 -88
- rbx/resources/packagers/boca/interactor_compile.sh +5 -2
- rbx/resources/packagers/boca/interactor_run.sh +174 -0
- rbx/resources/packagers/boca/safeexec.c +530 -0
- rbx/resources/packagers/boca/safeexec_compile.sh +49 -0
- rbx/resources/presets/default/contest/contest.rbx.yml +9 -8
- rbx/resources/presets/default/problem/problem.rbx.yml +27 -26
- rbx/resources/templates/rbx.h +2 -3
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/METADATA +2 -1
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/RECORD +66 -55
- rbx/resources/packagers/boca/compile/pas +0 -172
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/LICENSE +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/WHEEL +0 -0
- {rbx_cp-0.5.73.dist-info → rbx_cp-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -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
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
-
) ->
|
304
|
-
return
|
372
|
+
) -> BocaScraper:
|
373
|
+
return BocaScraper(base_url, username, password)
|
rbx/box/tooling/main.py
ADDED
@@ -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
|
-
|
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 =
|
353
|
-
commands=commands,
|
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
|
|
rbx/grading/judge/sandbox.py
CHANGED
@@ -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
|
-
|
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) ->
|
357
|
+
async def get_pid(self) -> int:
|
336
358
|
"""Return the PID of the sandboxed process.
|
337
359
|
|
338
|
-
|
339
|
-
|
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,
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
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
|
-
|
329
|
-
|
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)
|