Skip to content

Commit cabe503

Browse files
authoredJun 26, 2024
Migrate to cgroup2, debian:bookworm, Python 3.8+, aiohttp 3+, and ruamel.yaml 0.18.0+ (#87)
1 parent 592c55d commit cabe503

13 files changed

+156
-127
lines changed
 

‎.github/workflows/main.yml

+8-8
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ jobs:
1616
id: vars
1717
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
1818

19-
- name: Setup Python 3.5
19+
- name: Setup Python 3.8
2020
uses: actions/setup-python@v2
2121
with:
22-
python-version: 3.5
22+
python-version: 3.8
2323

2424
- name: Setup pip cache
2525
uses: actions/cache@v2
2626
with:
2727
path: ~/.cache/pip
28-
key: ${{ runner.os }}-py3.5-pip--${{ hashFiles('requirements.txt') }}
28+
key: ${{ runner.os }}-py3.8-pip--${{ hashFiles('requirements.txt') }}
2929
restore-keys: |
30-
${{ runner.os }}-py3.5-pip-
31-
${{ runner.os }}-py3.5-
30+
${{ runner.os }}-py3.8-pip-
31+
${{ runner.os }}-py3.8-
3232
3333
- name: Prepare environment
3434
run: |
@@ -39,7 +39,7 @@ jobs:
3939
4040
- name: Unit test
4141
run: python -m unittest -v jd4.case_test jd4.compare_test
42-
42+
4343
- name: Setup Docker Buildx
4444
uses: docker/setup-buildx-action@v1
4545

@@ -53,7 +53,7 @@ jobs:
5353
- name: Integration test
5454
run: |
5555
docker load --input /tmp/jd4.tar
56-
docker run --privileged \
56+
docker run --privileged --cgroupns=host \
5757
-v $(readlink -f examples/config.yaml):/root/.config/jd4/config.yaml \
5858
vijos/jd4 /bin/bash -c "source /venv/bin/activate && python3 -m unittest -v jd4.integration_test"
5959
@@ -93,7 +93,7 @@ jobs:
9393
with:
9494
push: true
9595
tags: vijos/jd4:latest,vijos/jd4:${{ needs.test.outputs.sha_short }}
96-
96+
9797
- name: Release to GitHub
9898
uses: ncipollo/release-action@v1
9999
with:

‎Dockerfile

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM debian:stretch
1+
FROM debian:bookworm
22
COPY . /tmp/jd4
33
RUN apt-get update && \
44
apt-get install -y \
@@ -8,11 +8,11 @@ RUN apt-get update && \
88
python3-dev \
99
g++ \
1010
fp-compiler \
11-
openjdk-8-jdk-headless \
12-
python \
13-
php7.0-cli \
11+
openjdk-17-jdk-headless \
12+
python-is-python3 \
13+
php8.2-cli \
1414
rustc \
15-
haskell-platform \
15+
ghc \
1616
libjavascriptcoregtk-4.0-bin \
1717
golang \
1818
ruby \

‎README.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Introduction
1212
jd4 is a judging daemon for programming contests like OI and ACM. It is called
1313
jd4 because we had jd, jd2, jd3 before. Unlike previous versions that use
1414
Windows sandboxing techniques, jd4 uses newer sandboxing facilities that
15-
appear in Linux 4.4+. jd4 also multiplexes most I/O on an event loop so that
15+
appear in Linux 5.19+. jd4 also multiplexes most I/O on an event loop so that
1616
only two extra threads are used during a judge - one for input, and one for
1717
output, thus allowing blocking custom judge implementations.
1818

@@ -21,7 +21,7 @@ Usage
2121

2222
Prerequisites:
2323

24-
- Linux 4.4+
24+
- Linux 5.19+
2525
- Docker
2626

2727
Put config.yaml in the configuration directory, usually in
@@ -40,8 +40,8 @@ Development
4040

4141
Prerequisites:
4242

43-
- Linux 4.4+
44-
- Python 3.5+
43+
- Linux 5.19+
44+
- Python 3.8+
4545

4646
Use the following command to install Python requirements::
4747

@@ -113,7 +113,7 @@ throughput increment by using Cython (like 3MB/s to 200MB/s).
113113
Copyright and License
114114
---------------------
115115

116-
Copyright (c) 2017 Vijos Dev Team. All rights reserved.
116+
Copyright (c) 2024 Vijos Dev Team. All rights reserved.
117117

118118
This program is free software: you can redistribute it and/or modify
119119
it under the terms of the GNU Affero General Public License as

‎jd4/_sandbox.pyx

+11-1
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@ def enter_namespace(root_dir, in_dir, out_dir):
5050
mkdir('dev')
5151
bind_mount('/dev/null', 'dev/null', False, True, True, False)
5252
bind_mount('/dev/urandom', 'dev/urandom', False, True, True, False)
53+
mkdir('.cache')
54+
mount('.cache', '.cache', 'tmpfs', MS_NOSUID, 'size=16m,nr_inodes=4k')
5355
mkdir('tmp')
54-
mount('tmp', 'tmp', 'tmpfs', MS_NOSUID, "size=16m,nr_inodes=4k")
56+
mount('tmp', 'tmp', 'tmpfs', MS_NOSUID, 'size=16m,nr_inodes=4k')
5557
bind_or_link('/bin', 'bin')
5658
bind_or_link('/etc/alternatives', 'etc/alternatives')
59+
bind_or_link('/etc/java-17-openjdk', 'etc/java-17-openjdk')
60+
bind_or_link('/etc/mono', 'etc/mono')
5761
bind_or_link('/lib', 'lib')
5862
bind_or_link('/lib64', 'lib64')
5963
bind_or_link('/usr/bin', 'usr/bin')
@@ -65,6 +69,12 @@ def enter_namespace(root_dir, in_dir, out_dir):
6569
bind_or_link('/var/lib/ghc', 'var/lib/ghc')
6670
bind_mount(in_dir, 'in', True, False, True, True)
6771
bind_mount(out_dir, 'out', True, False, True, False)
72+
write_text_file('etc/fpc.cfg', '''
73+
-Sgic
74+
-Fu/usr/lib/$fpctarget-gnu/fpc/$fpcversion/units/$fpctarget
75+
-Fu/usr/lib/$fpctarget-gnu/fpc/$fpcversion/units/$fpctarget/*
76+
-Fu/usr/lib/$fpctarget-gnu/fpc/$fpcversion/units/$fpctarget/rtl
77+
''')
6878
write_text_file('etc/passwd', 'icebox:x:1000:1000:icebox:/:/bin/bash\n')
6979
mkdir('old_root')
7080
pivot_root('.', 'old_root')

‎jd4/case.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
DEFAULT_TIME_NS = 1000000000
2424
DEFAULT_MEMORY_BYTES = 268435456
2525
PROCESS_LIMIT = 64
26+
_yaml_safe = yaml.YAML(typ='safe')
2627

2728
class CaseBase:
2829
def __init__(self, time_limit_ns, memory_limit_bytes, process_limit, score):
@@ -255,7 +256,7 @@ def read_legacy_cases(config, open):
255256
int(score_str))
256257

257258
def read_yaml_cases(config, open):
258-
for case in yaml.safe_load(config)['cases']:
259+
for case in _yaml_safe.load(config)['cases']:
259260
if 'judge' not in case:
260261
yield DefaultCase(partial(open, case['input']),
261262
partial(open, case['output']),

‎jd4/cgroup.py

+43-37
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from asyncio import get_event_loop, shield, sleep, wait_for, TimeoutError
22
from itertools import chain
3-
from os import access, cpu_count, geteuid, kill, makedirs, path, rmdir, W_OK
3+
from os import access, cpu_count, geteuid, getpid, kill, makedirs, path, rmdir, W_OK
44
from signal import SIGKILL
55
from socket import socket, AF_UNIX, SOCK_STREAM, SOL_SOCKET, SO_PEERCRED
66
from subprocess import call
@@ -10,58 +10,63 @@
1010
from jd4.log import logger
1111
from jd4.util import read_text_file, write_text_file
1212

13-
CPUACCT_CGROUP_ROOT = '/sys/fs/cgroup/cpuacct/jd4'
14-
MEMORY_CGROUP_ROOT = '/sys/fs/cgroup/memory/jd4'
15-
PIDS_CGROUP_ROOT = '/sys/fs/cgroup/pids/jd4'
13+
CGROUP2_ROOT = '/sys/fs/cgroup/jd4'
14+
CGROUP2_DAEMON_ROOT = path.join(CGROUP2_ROOT, 'daemon')
1615
WAIT_JITTER_NS = 5000000
1716

1817
def try_init_cgroup():
1918
euid = geteuid()
20-
cgroups_to_init = list()
21-
if not (path.isdir(CPUACCT_CGROUP_ROOT) and access(CPUACCT_CGROUP_ROOT, W_OK)):
22-
cgroups_to_init.append(CPUACCT_CGROUP_ROOT)
23-
if not (path.isdir(MEMORY_CGROUP_ROOT) and access(MEMORY_CGROUP_ROOT, W_OK)):
24-
cgroups_to_init.append(MEMORY_CGROUP_ROOT)
25-
if not (path.isdir(PIDS_CGROUP_ROOT) and access(PIDS_CGROUP_ROOT, W_OK)):
26-
cgroups_to_init.append(PIDS_CGROUP_ROOT)
27-
if cgroups_to_init:
19+
if not (path.isdir(CGROUP2_ROOT) and access(CGROUP2_ROOT, W_OK)):
20+
cgroup2_subtree_control = path.join(CGROUP2_ROOT, 'cgroup.subtree_control')
2821
if euid == 0:
29-
logger.info('Initializing cgroup: %s', ', '.join(cgroups_to_init))
30-
for cgroup_to_init in cgroups_to_init:
31-
makedirs(cgroup_to_init, exist_ok=True)
22+
logger.info('Initializing cgroup: %s', CGROUP2_ROOT)
23+
write_text_file('/sys/fs/cgroup/cgroup.subtree_control', '+cpu +memory +pids')
24+
makedirs(CGROUP2_ROOT, exist_ok=True)
25+
write_text_file(cgroup2_subtree_control, '+cpu +memory +pids')
26+
makedirs(CGROUP2_DAEMON_ROOT, exist_ok=True)
3227
elif __stdin__.isatty():
33-
logger.info('Initializing cgroup: %s', ', '.join(cgroups_to_init))
34-
call(['sudo', 'sh', '-c', 'mkdir -p "{1}" && chown -R "{0}" "{1}"'.format(
35-
euid, '" "'.join(cgroups_to_init))])
28+
logger.info('Initializing cgroup: %s', CGROUP2_ROOT)
29+
call(['sudo', 'sh', '-c',
30+
'''echo "+cpu +memory +pids" > "/sys/fs/cgroup/cgroup.subtree_control" &&
31+
mkdir -p "{1}" &&
32+
chown -R "{0}" "{1}" &&
33+
echo "+cpu +memory +pids" > "{2}" &&
34+
mkdir -p "{3}" &&
35+
chown -R "{0}" "{3}"'''.format(
36+
euid, CGROUP2_ROOT, cgroup2_subtree_control, CGROUP2_DAEMON_ROOT)])
3637
else:
3738
logger.error('Cgroup not initialized')
3839

40+
# Put myself into the cgroup that I can write.
41+
pid = getpid()
42+
cgroup2_daemon_procs = path.join(CGROUP2_DAEMON_ROOT, 'cgroup.procs')
43+
if euid == 0:
44+
logger.info('Entering cgroup: %s', CGROUP2_DAEMON_ROOT)
45+
write_text_file(cgroup2_daemon_procs, str(pid))
46+
elif __stdin__.isatty():
47+
logger.info('Entering cgroup: %s', CGROUP2_DAEMON_ROOT)
48+
call(['sudo', 'sh', '-c', 'echo "{0}" > "{1}"'.format(pid, cgroup2_daemon_procs)])
49+
else:
50+
logger.error('Cgroup not entered')
51+
3952
class CGroup:
4053
def __init__(self):
41-
self.cpuacct_cgroup_dir = mkdtemp(prefix='', dir=CPUACCT_CGROUP_ROOT)
42-
self.memory_cgroup_dir = mkdtemp(prefix='', dir=MEMORY_CGROUP_ROOT)
43-
self.pids_cgroup_dir = mkdtemp(prefix='', dir=PIDS_CGROUP_ROOT)
54+
self.cgroup2_dir = mkdtemp(prefix='', dir=CGROUP2_ROOT)
4455

4556
def close(self):
46-
rmdir(self.cpuacct_cgroup_dir)
47-
rmdir(self.memory_cgroup_dir)
48-
rmdir(self.pids_cgroup_dir)
57+
rmdir(self.cgroup2_dir)
4958

5059
async def accept(self, sock):
5160
loop = get_event_loop()
5261
accept_sock, _ = await loop.sock_accept(sock)
5362
pid = accept_sock.getsockopt(SOL_SOCKET, SO_PEERCRED)
54-
write_text_file(path.join(self.cpuacct_cgroup_dir, 'tasks'), str(pid))
55-
write_text_file(path.join(self.memory_cgroup_dir, 'tasks'), str(pid))
56-
write_text_file(path.join(self.pids_cgroup_dir, 'tasks'), str(pid))
63+
write_text_file(path.join(self.cgroup2_dir, 'cgroup.procs'), str(pid))
5764
accept_sock.close()
5865

5966
@property
6067
def procs(self):
61-
return set(chain(
62-
map(int, read_text_file(path.join(self.cpuacct_cgroup_dir, 'cgroup.procs')).splitlines()),
63-
map(int, read_text_file(path.join(self.memory_cgroup_dir, 'cgroup.procs')).splitlines()),
64-
map(int, read_text_file(path.join(self.pids_cgroup_dir, 'cgroup.procs')).splitlines())))
68+
return set(map(int,
69+
read_text_file(path.join(self.cgroup2_dir, 'cgroup.procs')).splitlines()))
6570

6671
def kill(self):
6772
procs = self.procs
@@ -77,27 +82,28 @@ def kill(self):
7782

7883
@property
7984
def cpu_usage_ns(self):
80-
return int(read_text_file(path.join(self.cpuacct_cgroup_dir, 'cpuacct.usage')))
85+
return 1000 * int(read_text_file(path.join(self.cgroup2_dir, 'cpu.stat'))
86+
.splitlines()[0].split()[1])
8187

8288
@property
8389
def memory_limit_bytes(self):
84-
return int(read_text_file(path.join(self.memory_cgroup_dir, 'memory.limit_in_bytes')))
90+
return int(read_text_file(path.join(self.cgroup2_dir, 'memory.max')))
8591

8692
@memory_limit_bytes.setter
8793
def memory_limit_bytes(self, value):
88-
write_text_file(path.join(self.memory_cgroup_dir, 'memory.limit_in_bytes'), str(value))
94+
write_text_file(path.join(self.cgroup2_dir, 'memory.max'), str(value))
8995

9096
@property
9197
def memory_usage_bytes(self):
92-
return int(read_text_file(path.join(self.memory_cgroup_dir, 'memory.max_usage_in_bytes')))
98+
return int(read_text_file(path.join(self.cgroup2_dir, 'memory.peak')))
9399

94100
@property
95101
def pids_max(self):
96-
return int(read_text_file(path.join(self.pids_cgroup_dir, 'pids.max')))
102+
return int(read_text_file(path.join(self.cgroup2_dir, 'pids.max')))
97103

98104
@pids_max.setter
99105
def pids_max(self, value):
100-
write_text_file(path.join(self.pids_cgroup_dir, 'pids.max'), str(value))
106+
write_text_file(path.join(self.cgroup2_dir, 'pids.max'), str(value))
101107

102108
def enter_cgroup(socket_path):
103109
with socket(AF_UNIX, SOCK_STREAM) as sock:

‎jd4/compile.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
_CONFIG_DIR = user_config_dir('jd4')
2323
_LANGS_FILE = path.join(_CONFIG_DIR, 'langs.yaml')
2424
_langs = dict()
25+
_yaml = yaml.YAML()
2526

2627
class Executable:
2728
def __init__(self, execute_file, execute_args):
@@ -155,7 +156,7 @@ async def build(lang, code):
155156
def _init():
156157
try:
157158
with open(_LANGS_FILE) as file:
158-
langs_config = yaml.load(file, Loader=yaml.RoundTripLoader)
159+
langs_config = _yaml.load(file)
159160
except FileNotFoundError:
160161
logger.error('Language file %s not found.', _LANGS_FILE)
161162
exit(1)

‎jd4/config.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77

88
_CONFIG_DIR = user_config_dir('jd4')
99
_CONFIG_FILE = path.join(_CONFIG_DIR, 'config.yaml')
10+
_yaml = yaml.YAML()
1011

1112
def _load_config():
1213
try:
1314
with open(_CONFIG_FILE, encoding='utf-8') as file:
14-
return yaml.load(file, Loader=yaml.RoundTripLoader)
15+
return _yaml.load(file)
1516
except FileNotFoundError:
1617
logger.error('Config file %s not found.', _CONFIG_FILE)
1718
exit(1)
@@ -21,7 +22,7 @@ def _load_config():
2122
async def save_config():
2223
def do_save_config():
2324
with open(_CONFIG_FILE, 'w', encoding='utf-8') as file:
24-
yaml.dump(config, file, Dumper=yaml.RoundTripDumper)
25+
_yaml.dump(config, file)
2526

2627
await get_event_loop().run_in_executor(None, do_save_config)
2728

0 commit comments

Comments
 (0)