docker-evaluator 0.1.0__tar.gz

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 (37) hide show
  1. docker_evaluator-0.1.0/PKG-INFO +125 -0
  2. docker_evaluator-0.1.0/README.md +112 -0
  3. docker_evaluator-0.1.0/docker_evaluator/__init__.py +6 -0
  4. docker_evaluator-0.1.0/docker_evaluator/disk_helper.py +41 -0
  5. docker_evaluator-0.1.0/docker_evaluator/docker_helper.py +50 -0
  6. docker_evaluator-0.1.0/docker_evaluator/env_helper.py +8 -0
  7. docker_evaluator-0.1.0/docker_evaluator/evaluator.py +49 -0
  8. docker_evaluator-0.1.0/docker_evaluator/language_helpers/__init__.py +0 -0
  9. docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/Dockerfile +7 -0
  10. docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/__init__.py +0 -0
  11. docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/c_helper.py +8 -0
  12. docker_evaluator-0.1.0/docker_evaluator/language_helpers/c_helper/entrypoint.sh +56 -0
  13. docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/Dockerfile +7 -0
  14. docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/__init__.py +0 -0
  15. docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/cpp_helper.py +8 -0
  16. docker_evaluator-0.1.0/docker_evaluator/language_helpers/cpp_helper/entrypoint.sh +56 -0
  17. docker_evaluator-0.1.0/docker_evaluator/language_helpers/language_helper.py +83 -0
  18. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/Dockerfile +7 -0
  19. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/__init__.py +0 -0
  20. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/entrypoint.sh +39 -0
  21. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py2_helper/py2_helper.py +8 -0
  22. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/Dockerfile +7 -0
  23. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/__init__.py +0 -0
  24. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/entrypoint.sh +39 -0
  25. docker_evaluator-0.1.0/docker_evaluator/language_helpers/py3_helper/py3_helper.py +8 -0
  26. docker_evaluator-0.1.0/docker_evaluator.egg-info/PKG-INFO +125 -0
  27. docker_evaluator-0.1.0/docker_evaluator.egg-info/SOURCES.txt +35 -0
  28. docker_evaluator-0.1.0/docker_evaluator.egg-info/dependency_links.txt +1 -0
  29. docker_evaluator-0.1.0/docker_evaluator.egg-info/requires.txt +7 -0
  30. docker_evaluator-0.1.0/docker_evaluator.egg-info/top_level.txt +1 -0
  31. docker_evaluator-0.1.0/pyproject.toml +49 -0
  32. docker_evaluator-0.1.0/setup.cfg +4 -0
  33. docker_evaluator-0.1.0/setup.py +3 -0
  34. docker_evaluator-0.1.0/tests/test_disk_helper.py +97 -0
  35. docker_evaluator-0.1.0/tests/test_docker_helper.py +115 -0
  36. docker_evaluator-0.1.0/tests/test_evaluator.py +125 -0
  37. docker_evaluator-0.1.0/tests/test_language_helper.py +183 -0
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: docker-evaluator
3
+ Version: 0.1.0
4
+ Summary: Generic code evaluation in isolated Docker containers
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: docker>=7.1.0
8
+ Requires-Dist: python-dotenv>=1.0.1
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == "dev"
11
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
12
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
13
+
14
+ # docker-evaluator
15
+
16
+ A code evaluation backend for competitive programming judges. Runs untrusted submissions in isolated Docker containers, enforces time and memory limits, and checks output against expected results — similar to how Codeforces/CodeChef judge submissions.
17
+
18
+ Supports Python 2/3, C, and C++.
19
+
20
+ ## Requirements
21
+
22
+ - Docker (running and accessible to the current user)
23
+ - Python 3.9+
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install docker-evaluator
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from docker_evaluator import DockerEvaluator
35
+
36
+ evaluator = DockerEvaluator()
37
+
38
+ result = evaluator.evaluate(
39
+ code='n = int(input()); print(n * 2)',
40
+ input="21",
41
+ expected_output="42",
42
+ language="py3",
43
+ time_limit=1,
44
+ memory_limit=256 * 1024, # 256 MB in KB
45
+ )
46
+
47
+ print(result)
48
+ # {'correct': True, 'details': 'OK (8ms)'}
49
+
50
+ evaluator.close()
51
+ ```
52
+
53
+ ### Parameters
54
+
55
+ | Parameter | Type | Default | Description |
56
+ |---|---|---|---|
57
+ | `code` | `str` | — | Submission source code |
58
+ | `input` | `str` | — | Problem input (stdin or file content) |
59
+ | `expected_output` | `str` | — | Correct output to compare against |
60
+ | `language` | `str` | — | `"py3"`, `"py2"`, `"c"`, or `"cpp"` |
61
+ | `time_limit` | `int` | — | Time limit in seconds |
62
+ | `input_type` | `str` | `"stdin"` | `"stdin"` or `"file"` |
63
+ | `file_io_name` | `str` | `""` | File name when using file I/O (e.g. `"input.txt"`) |
64
+ | `memory_limit` | `int` | `1024` | Memory limit in KB (minimum enforced: 256 MB) |
65
+
66
+ ### Return value
67
+
68
+ ```python
69
+ {"correct": bool, "details": str}
70
+ ```
71
+
72
+ `details` is one of:
73
+
74
+ - `OK (Xms)` — accepted
75
+ - `Wrong Answer`
76
+ - `Time Limit Exceeded`
77
+ - `Memory Limit Exceeded`
78
+ - `Runtime Error (exit code N)`
79
+ - `Compilation Error`
80
+
81
+ ## Supported languages
82
+
83
+ | Key | Language |
84
+ |---|---|
85
+ | `py3` | Python 3 |
86
+ | `py2` | Python 2 |
87
+ | `c` | C |
88
+ | `cpp` | C++ |
89
+
90
+ Docker images are built automatically on first use and cached for subsequent runs.
91
+
92
+ ## File I/O
93
+
94
+ For problems that read/write files instead of stdin/stdout:
95
+
96
+ ```python
97
+ result = evaluator.evaluate(
98
+ code=open("solution.cpp").read(),
99
+ input="5\n1 2 3 4 5",
100
+ expected_output="15",
101
+ language="cpp",
102
+ time_limit=2,
103
+ input_type="file",
104
+ file_io_name="input.txt",
105
+ )
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ Create a `.env` file in your working directory:
111
+
112
+ | Variable | Default | Description |
113
+ |---|---|---|
114
+ | `KEEP_EVAL_CONTAINERS` | `0` | Set to `1` to keep containers after a run (useful for debugging) |
115
+ | `ENVIRONMENT` | — | If set, also loads `.env.<ENVIRONMENT>` |
116
+
117
+ ## Compilation cache
118
+
119
+ C and C++ submissions are cached by source hash — repeated evaluations of the same code skip recompilation. To clear the cache:
120
+
121
+ ```python
122
+ from docker_evaluator import clear_cache
123
+
124
+ clear_cache()
125
+ ```
@@ -0,0 +1,112 @@
1
+ # docker-evaluator
2
+
3
+ A code evaluation backend for competitive programming judges. Runs untrusted submissions in isolated Docker containers, enforces time and memory limits, and checks output against expected results — similar to how Codeforces/CodeChef judge submissions.
4
+
5
+ Supports Python 2/3, C, and C++.
6
+
7
+ ## Requirements
8
+
9
+ - Docker (running and accessible to the current user)
10
+ - Python 3.9+
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install docker-evaluator
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```python
21
+ from docker_evaluator import DockerEvaluator
22
+
23
+ evaluator = DockerEvaluator()
24
+
25
+ result = evaluator.evaluate(
26
+ code='n = int(input()); print(n * 2)',
27
+ input="21",
28
+ expected_output="42",
29
+ language="py3",
30
+ time_limit=1,
31
+ memory_limit=256 * 1024, # 256 MB in KB
32
+ )
33
+
34
+ print(result)
35
+ # {'correct': True, 'details': 'OK (8ms)'}
36
+
37
+ evaluator.close()
38
+ ```
39
+
40
+ ### Parameters
41
+
42
+ | Parameter | Type | Default | Description |
43
+ |---|---|---|---|
44
+ | `code` | `str` | — | Submission source code |
45
+ | `input` | `str` | — | Problem input (stdin or file content) |
46
+ | `expected_output` | `str` | — | Correct output to compare against |
47
+ | `language` | `str` | — | `"py3"`, `"py2"`, `"c"`, or `"cpp"` |
48
+ | `time_limit` | `int` | — | Time limit in seconds |
49
+ | `input_type` | `str` | `"stdin"` | `"stdin"` or `"file"` |
50
+ | `file_io_name` | `str` | `""` | File name when using file I/O (e.g. `"input.txt"`) |
51
+ | `memory_limit` | `int` | `1024` | Memory limit in KB (minimum enforced: 256 MB) |
52
+
53
+ ### Return value
54
+
55
+ ```python
56
+ {"correct": bool, "details": str}
57
+ ```
58
+
59
+ `details` is one of:
60
+
61
+ - `OK (Xms)` — accepted
62
+ - `Wrong Answer`
63
+ - `Time Limit Exceeded`
64
+ - `Memory Limit Exceeded`
65
+ - `Runtime Error (exit code N)`
66
+ - `Compilation Error`
67
+
68
+ ## Supported languages
69
+
70
+ | Key | Language |
71
+ |---|---|
72
+ | `py3` | Python 3 |
73
+ | `py2` | Python 2 |
74
+ | `c` | C |
75
+ | `cpp` | C++ |
76
+
77
+ Docker images are built automatically on first use and cached for subsequent runs.
78
+
79
+ ## File I/O
80
+
81
+ For problems that read/write files instead of stdin/stdout:
82
+
83
+ ```python
84
+ result = evaluator.evaluate(
85
+ code=open("solution.cpp").read(),
86
+ input="5\n1 2 3 4 5",
87
+ expected_output="15",
88
+ language="cpp",
89
+ time_limit=2,
90
+ input_type="file",
91
+ file_io_name="input.txt",
92
+ )
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ Create a `.env` file in your working directory:
98
+
99
+ | Variable | Default | Description |
100
+ |---|---|---|
101
+ | `KEEP_EVAL_CONTAINERS` | `0` | Set to `1` to keep containers after a run (useful for debugging) |
102
+ | `ENVIRONMENT` | — | If set, also loads `.env.<ENVIRONMENT>` |
103
+
104
+ ## Compilation cache
105
+
106
+ C and C++ submissions are cached by source hash — repeated evaluations of the same code skip recompilation. To clear the cache:
107
+
108
+ ```python
109
+ from docker_evaluator import clear_cache
110
+
111
+ clear_cache()
112
+ ```
@@ -0,0 +1,6 @@
1
+ from docker_evaluator.disk_helper import clear_cache
2
+ from docker_evaluator.docker_helper import DockerHelper
3
+ from docker_evaluator.env_helper import load_env_variables
4
+ from docker_evaluator.evaluator import DockerEvaluator
5
+
6
+ __all__ = ["DockerEvaluator", "DockerHelper", "clear_cache", "load_env_variables"]
@@ -0,0 +1,41 @@
1
+ import hashlib
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import threading
6
+
7
+ _compile_locks = {}
8
+ _compile_locks_mutex = threading.Lock()
9
+
10
+
11
+ def get_compile_lock(cache_dir):
12
+ with _compile_locks_mutex:
13
+ if cache_dir not in _compile_locks:
14
+ _compile_locks[cache_dir] = threading.Lock()
15
+ return _compile_locks[cache_dir]
16
+
17
+
18
+ _CACHE_BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "compilation_cache"))
19
+
20
+
21
+ def get_cache_dir(code, language):
22
+ code_hash = hashlib.sha256(code.encode()).hexdigest()
23
+ cache_dir = os.path.join(_CACHE_BASE, language, code_hash)
24
+ os.makedirs(cache_dir, exist_ok=True)
25
+ return cache_dir
26
+
27
+
28
+ def clear_cache():
29
+ if os.path.exists(_CACHE_BASE):
30
+ shutil.rmtree(_CACHE_BASE)
31
+ print("Compilation cache cleared.")
32
+
33
+
34
+ def get_temp_dir(files):
35
+ temp_dir = tempfile.mkdtemp()
36
+ test_data_dir = os.path.join(temp_dir, "test_data")
37
+ os.makedirs(test_data_dir, exist_ok=True)
38
+ for file in files:
39
+ with open(os.path.join(test_data_dir, file["name"]), "w", encoding="utf-8", errors="replace") as file_writer:
40
+ file_writer.write(file["content"])
41
+ return test_data_dir
@@ -0,0 +1,50 @@
1
+ import os
2
+
3
+ import docker
4
+ from docker.errors import ContainerError
5
+ from docker.types import LogConfig
6
+
7
+
8
+ class DockerHelper:
9
+ def __init__(self):
10
+ self.client = docker.from_env()
11
+
12
+ def image_exists(self, image_tag):
13
+ images = self.client.images.list()
14
+ image_names = []
15
+ for image in images:
16
+ image_names.extend(image.tags)
17
+ return image_tag in image_names
18
+
19
+ def create_image(self, image_path, image_tag):
20
+ self.client.images.build(path=image_path, tag=image_tag)
21
+
22
+ def evaluate(self, image_name, volume, environment_variables, cpus=1, memory_limit_mb=None, cache_dir=None):
23
+ volumes = {volume: {"bind": "/test_data", "mode": "ro"}}
24
+ if cache_dir:
25
+ volumes[cache_dir] = {"bind": "/cache", "mode": "rw"}
26
+ # Read env at call time because env files may be loaded after helper init.
27
+ keep_eval_containers = os.getenv("KEEP_EVAL_CONTAINERS", "0") == "1"
28
+ try:
29
+ logs = self.client.containers.run(
30
+ image_name,
31
+ volumes=volumes,
32
+ detach=False,
33
+ environment=environment_variables,
34
+ remove=not keep_eval_containers,
35
+ log_config=LogConfig(type="json-file"),
36
+ nano_cpus=int(cpus * 1e9),
37
+ mem_limit=f"{memory_limit_mb}m" if memory_limit_mb else None,
38
+ memswap_limit=f"{memory_limit_mb}m" if memory_limit_mb else None,
39
+ network_disabled=True,
40
+ pids_limit=64,
41
+ )
42
+ except ContainerError as e:
43
+ if e.exit_status == 137:
44
+ return "Memory Limit Exceeded"
45
+ return f"Runtime Error (exit code {e.exit_status})"
46
+ output = logs.decode("utf-8", errors="replace").rstrip()
47
+ return output
48
+
49
+ def close(self):
50
+ self.client.close()
@@ -0,0 +1,8 @@
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+
5
+
6
+ def load_env_variables():
7
+ load_dotenv(".env")
8
+ load_dotenv(f".env.{os.getenv('ENVIRONMENT')}")
@@ -0,0 +1,49 @@
1
+ from docker_evaluator.docker_helper import DockerHelper
2
+ from docker_evaluator.env_helper import load_env_variables
3
+ from docker_evaluator.language_helpers.c_helper.c_helper import CHelper
4
+ from docker_evaluator.language_helpers.cpp_helper.cpp_helper import CppHelper
5
+ from docker_evaluator.language_helpers.py2_helper.py2_helper import Py2Helper
6
+ from docker_evaluator.language_helpers.py3_helper.py3_helper import Py3Helper
7
+
8
+
9
+ class DockerEvaluator:
10
+ def __init__(self, docker_client=None):
11
+ load_env_variables()
12
+ self.docker_helper = docker_client
13
+ if docker_client is None:
14
+ self.docker_helper = DockerHelper()
15
+
16
+ self.language_helpers = [
17
+ CHelper(self.docker_helper),
18
+ CppHelper(self.docker_helper),
19
+ Py2Helper(self.docker_helper),
20
+ Py3Helper(self.docker_helper),
21
+ ]
22
+
23
+ def evaluate(
24
+ self, code, input, expected_output, language, time_limit, input_type="stdin", file_io_name="", memory_limit=1024
25
+ ):
26
+ # Enforce minimum memory limit of 256MB to avoid spurious segfaults on valid code.
27
+ # memory_limit is in KB, so 262144 KB = 256 MB.
28
+ MIN_MEMORY_KB = 256 * 1024
29
+ memory_limit = max(memory_limit, MIN_MEMORY_KB)
30
+
31
+ for language_helper in self.language_helpers:
32
+ if language_helper.language == language:
33
+ output = language_helper.evaluate(
34
+ code, input, time_limit, input_type, file_io_name, memory_limit=memory_limit
35
+ )
36
+ # Extract container-side timing appended by entrypoint as last line
37
+ time_str = None
38
+ lines = output.split("\n")
39
+ if lines and lines[-1].startswith("__TIME__:"):
40
+ time_str = lines[-1][len("__TIME__:") :]
41
+ output = "\n".join(lines[:-1]).rstrip()
42
+ if "Limit Exceeded" in output or "Compilation Error" in output or "Runtime Error" in output:
43
+ return {"correct": False, "details": output}
44
+ elif output.split() != expected_output.split():
45
+ return {"correct": False, "details": "Wrong Answer"}
46
+ return {"correct": True, "details": f"OK ({time_str})" if time_str else "OK (time unavailable)"}
47
+
48
+ def close(self):
49
+ self.docker_helper.close()
@@ -0,0 +1,7 @@
1
+ FROM gcc:latest
2
+
3
+ WORKDIR /app
4
+
5
+ COPY entrypoint.sh .
6
+ RUN chmod +x ./entrypoint.sh
7
+ ENTRYPOINT ["./entrypoint.sh"]
@@ -0,0 +1,8 @@
1
+ import os
2
+
3
+ from docker_evaluator.language_helpers.language_helper import LanguageHelper
4
+
5
+
6
+ class CHelper(LanguageHelper):
7
+ def __init__(self, docker_helper):
8
+ super().__init__(docker_helper, os.path.dirname(__file__), "c", "c", 1, cache_compilation=True)
@@ -0,0 +1,56 @@
1
+ #!/bin/sh
2
+ if [ -f /cache/main ]; then
3
+ cp /cache/main ./main
4
+ else
5
+ compile_output=$(gcc -std=c99 -O2 -o ./main /test_data/target.c 2>&1)
6
+ if [ $? -ne 0 ]; then
7
+ echo "Compilation Error: $compile_output"
8
+ exit 0
9
+ fi
10
+ cp ./main /cache/main 2>/dev/null || true
11
+ fi
12
+
13
+ cp /test_data/target.in ./target.in
14
+
15
+ start_ms=$(date +%s%3N)
16
+ if [ "$INPUT_TYPE" = "file" ]; then
17
+ cp ./target.in ./${FILE_IO_NAME}.in
18
+ if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
19
+ timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main"
20
+ else
21
+ timeout -k 1 ${TIME_LIMIT} ./main
22
+ fi
23
+ exit_code=$?
24
+ else
25
+ if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
26
+ timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main" < ./target.in > ./result.out
27
+ else
28
+ timeout -k 1 ${TIME_LIMIT} ./main < ./target.in > ./result.out
29
+ fi
30
+ exit_code=$?
31
+ fi
32
+ end_ms=$(date +%s%3N)
33
+ elapsed_ms=$((end_ms - start_ms))
34
+
35
+ if [ $exit_code -eq 124 ]; then
36
+ echo "Time Limit Exceeded"
37
+ exit 0
38
+ fi
39
+
40
+ if [ $exit_code -eq 137 ]; then
41
+ echo "Memory Limit Exceeded"
42
+ exit 0
43
+ fi
44
+
45
+ if [ $exit_code -ne 0 ]; then
46
+ echo "Runtime Error (exit code $exit_code)"
47
+ exit 0
48
+ fi
49
+
50
+ if [ "$INPUT_TYPE" = "file" ]; then
51
+ cat ./${FILE_IO_NAME}.out 2>/dev/null
52
+ else
53
+ cat ./result.out
54
+ fi
55
+ printf '\n__TIME__:%sms\n' "${elapsed_ms}"
56
+ exit 0
@@ -0,0 +1,7 @@
1
+ FROM gcc:latest
2
+
3
+ WORKDIR /app
4
+
5
+ COPY entrypoint.sh .
6
+ RUN chmod +x ./entrypoint.sh
7
+ ENTRYPOINT ["./entrypoint.sh"]
@@ -0,0 +1,8 @@
1
+ import os
2
+
3
+ from docker_evaluator.language_helpers.language_helper import LanguageHelper
4
+
5
+
6
+ class CppHelper(LanguageHelper):
7
+ def __init__(self, docker_helper):
8
+ super().__init__(docker_helper, os.path.dirname(__file__), "cpp", "cpp", 1, cache_compilation=True)
@@ -0,0 +1,56 @@
1
+ #!/bin/sh
2
+ if [ -f /cache/main ]; then
3
+ cp /cache/main ./main
4
+ else
5
+ compile_output=$(g++ -std=c++20 -O2 -o ./main /test_data/target.cpp 2>&1)
6
+ if [ $? -ne 0 ]; then
7
+ echo "Compilation Error: $compile_output"
8
+ exit 0
9
+ fi
10
+ cp ./main /cache/main 2>/dev/null || true
11
+ fi
12
+
13
+ cp /test_data/target.in ./target.in
14
+
15
+ start_ms=$(date +%s%3N)
16
+ if [ "$INPUT_TYPE" = "file" ]; then
17
+ cp ./target.in ./${FILE_IO_NAME}.in
18
+ if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
19
+ timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main"
20
+ else
21
+ timeout -k 1 ${TIME_LIMIT} ./main
22
+ fi
23
+ exit_code=$?
24
+ else
25
+ if [ -n "$MEMORY_LIMIT_KB" ] && [ "$MEMORY_LIMIT_KB" -gt 0 ] 2>/dev/null; then
26
+ timeout -k 1 ${TIME_LIMIT} sh -c "ulimit -v \"$MEMORY_LIMIT_KB\" && exec ./main" < ./target.in > ./result.out
27
+ else
28
+ timeout -k 1 ${TIME_LIMIT} ./main < ./target.in > ./result.out
29
+ fi
30
+ exit_code=$?
31
+ fi
32
+ end_ms=$(date +%s%3N)
33
+ elapsed_ms=$((end_ms - start_ms))
34
+
35
+ if [ $exit_code -eq 124 ]; then
36
+ echo "Time Limit Exceeded"
37
+ exit 0
38
+ fi
39
+
40
+ if [ $exit_code -eq 137 ]; then
41
+ echo "Memory Limit Exceeded"
42
+ exit 0
43
+ fi
44
+
45
+ if [ $exit_code -ne 0 ]; then
46
+ echo "Runtime Error (exit code $exit_code)"
47
+ exit 0
48
+ fi
49
+
50
+ if [ "$INPUT_TYPE" = "file" ]; then
51
+ cat ./${FILE_IO_NAME}.out 2>/dev/null
52
+ else
53
+ cat ./result.out
54
+ fi
55
+ printf '\n__TIME__:%sms\n' "${elapsed_ms}"
56
+ exit 0
@@ -0,0 +1,83 @@
1
+ import os
2
+
3
+ from docker_evaluator.disk_helper import get_cache_dir, get_compile_lock, get_temp_dir
4
+
5
+
6
+ class LanguageHelper:
7
+ def __init__(
8
+ self,
9
+ docker_helper,
10
+ docker_context_path,
11
+ language,
12
+ file_extension,
13
+ language_time_limit_multiplier,
14
+ memory_overhead_mb=32,
15
+ cache_compilation=False,
16
+ ):
17
+ docker_image_name = f"docker-evaluator-{language}"
18
+ self.docker_helper = docker_helper
19
+ self.docker_image_name = docker_image_name
20
+ self.language = language
21
+ self.language_time_limit_multiplier = language_time_limit_multiplier
22
+ self.memory_overhead_mb = memory_overhead_mb
23
+ self.cache_compilation = cache_compilation
24
+ self.file_extension = file_extension
25
+ self.initialize(docker_context_path, docker_image_name)
26
+
27
+ def initialize(self, docker_context_path, docker_image_name):
28
+ docker_image_tag = f"{docker_image_name}:latest"
29
+ if not self.docker_helper.image_exists(docker_image_tag):
30
+ print(f"Image {docker_image_tag} not found, building...")
31
+ self.docker_helper.create_image(docker_context_path, docker_image_tag)
32
+ print(f"Image {docker_image_tag} built successfully.")
33
+
34
+ def evaluate(self, code, code_input, time_limit, input_type="stdin", file_io_name="", memory_limit=1024):
35
+ files = [
36
+ {"name": f"target.{self.file_extension}", "content": code},
37
+ {"name": "target.in", "content": code_input},
38
+ ]
39
+ # Add grace to compensate for Docker/WSL2 scheduling jitter on Windows.
40
+ # timeout measures wall clock, not CPU time, so the process may get less
41
+ # than a full CPU-second per wall-second under load.
42
+ GRACE_S = 0.2
43
+ effective_time_limit = time_limit * self.language_time_limit_multiplier + GRACE_S
44
+ environment_variables = {
45
+ "TIME_LIMIT": effective_time_limit,
46
+ "LANG": "C.UTF-8",
47
+ "LC_ALL": "C.UTF-8",
48
+ "INPUT_TYPE": input_type,
49
+ "FILE_IO_NAME": file_io_name or "",
50
+ "MEMORY_LIMIT_KB": str(memory_limit),
51
+ }
52
+ memory_limit_mb = memory_limit // 1024
53
+ total_memory_mb = memory_limit_mb + self.memory_overhead_mb
54
+ # C/C++ compilation can briefly use much more memory than runtime.
55
+ # Keep a minimum container budget for compile-heavy languages, while
56
+ # entrypoints enforce the requested runtime limit with ulimit.
57
+ compile_safe_memory_mb = max(total_memory_mb, 1536) if self.cache_compilation else total_memory_mb
58
+ temp_dir = get_temp_dir(files)
59
+ cache_dir = get_cache_dir(code, self.language) if self.cache_compilation else None
60
+ cache_file = os.path.join(cache_dir, "main") if cache_dir else None
61
+ cache_status = "disabled" if not cache_dir else ("hit" if os.path.exists(cache_file) else "miss")
62
+ print(
63
+ f"env: {environment_variables}, time: {time_limit}s x{self.language_time_limit_multiplier} +{GRACE_S}s grace = {effective_time_limit}s, memory: {memory_limit}KB ({memory_limit_mb}MB) + {self.memory_overhead_mb}MB overhead = {total_memory_mb}MB, container: {compile_safe_memory_mb}MB, cache: {cache_status}"
64
+ )
65
+ # On a cache miss, serialize via a per-hash lock so only one container
66
+ # compiles at a time. This prevents simultaneous writes to the same
67
+ # Windows volume path from hanging. Cache hits run without the lock.
68
+ if cache_dir and not os.path.exists(cache_file):
69
+ with get_compile_lock(cache_dir):
70
+ return self.docker_helper.evaluate(
71
+ self.docker_image_name,
72
+ temp_dir,
73
+ environment_variables=environment_variables,
74
+ memory_limit_mb=compile_safe_memory_mb,
75
+ cache_dir=cache_dir,
76
+ )
77
+ return self.docker_helper.evaluate(
78
+ self.docker_image_name,
79
+ temp_dir,
80
+ environment_variables=environment_variables,
81
+ memory_limit_mb=compile_safe_memory_mb,
82
+ cache_dir=cache_dir,
83
+ )
@@ -0,0 +1,7 @@
1
+ FROM python:2
2
+
3
+ WORKDIR /app
4
+
5
+ COPY entrypoint.sh .
6
+ RUN chmod +x ./entrypoint.sh
7
+ ENTRYPOINT ["./entrypoint.sh"]