volsu-contests/backend/app/services/judge.py
2025-11-30 19:55:50 +03:00

246 lines
8.6 KiB
Python

import httpx
from app.config import settings
# Mapping Judge0 language_id to Piston language name and version
# Updated with latest available versions in Piston
LANGUAGE_MAP = {
# Python
71: ("python", "3.12.0"), # Python 3.12 LTS
70: ("python", "3.12.0"), # Python 2 -> redirect to Python 3
# C/C++
50: ("c", "10.2.0"), # C (GCC 10.2.0)
54: ("c++", "10.2.0"), # C++ (GCC 10.2.0)
# Java
62: ("java", "15.0.2"), # Java (OpenJDK 15.0.2)
# JavaScript/Node.js
63: ("javascript", "20.11.1"), # Node.js 20.11.1
# Go
60: ("go", "1.16.2"), # Go 1.16.2
# Rust
73: ("rust", "1.68.2"), # Rust 1.68.2
# Kotlin
78: ("kotlin", "1.8.20"), # Kotlin 1.8.20
}
class JudgeStatus:
"""Status codes compatible with Judge0 format"""
IN_QUEUE = 1
PROCESSING = 2
ACCEPTED = 3
WRONG_ANSWER = 4
TIME_LIMIT_EXCEEDED = 5
COMPILATION_ERROR = 6
RUNTIME_ERROR_SIGSEGV = 7
RUNTIME_ERROR_SIGXFSZ = 8
RUNTIME_ERROR_SIGFPE = 9
RUNTIME_ERROR_SIGABRT = 10
RUNTIME_ERROR_NZEC = 11
RUNTIME_ERROR_OTHER = 12
INTERNAL_ERROR = 13
EXEC_FORMAT_ERROR = 14
class JudgeService:
"""Code execution service using Piston API"""
def __init__(self):
self.base_url = settings.piston_url
async def get_languages(self) -> list[dict]:
"""Get list of supported languages from Piston"""
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.base_url}/api/v2/runtimes")
response.raise_for_status()
runtimes = response.json()
# Create a set of available runtimes for quick lookup
available = {rt["language"] for rt in runtimes}
# Return only languages from LANGUAGE_MAP that are installed
result = []
seen_ids = set()
for lang_id, (piston_lang, piston_ver) in LANGUAGE_MAP.items():
if piston_lang in available and lang_id not in seen_ids:
seen_ids.add(lang_id)
# Find actual installed version
actual_version = piston_ver
for rt in runtimes:
if rt["language"] == piston_lang:
actual_version = rt["version"]
break
# Human-readable names
name_map = {
"python": "Python",
"c": "C (GCC)",
"c++": "C++ (GCC)",
"java": "Java",
"javascript": "JavaScript (Node.js)",
"go": "Go",
"rust": "Rust",
"kotlin": "Kotlin",
}
display_name = name_map.get(piston_lang, piston_lang.title())
result.append({
"id": lang_id,
"name": f"{display_name} ({actual_version})",
})
return result
def _normalize_output(self, output: str) -> str:
"""
Normalize output for comparison:
- Strip trailing whitespace from each line
- Ensure single trailing newline (as print() adds)
"""
if not output:
return ""
lines = output.splitlines()
normalized = "\n".join(line.rstrip() for line in lines)
if normalized:
normalized += "\n"
return normalized
def _get_piston_language(self, language_id: int) -> tuple[str, str]:
"""Convert Judge0 language_id to Piston language name and version"""
if language_id in LANGUAGE_MAP:
return LANGUAGE_MAP[language_id]
# Default to Python if unknown
return ("python", "3.10.0")
async def submit(
self,
source_code: str,
language_id: int,
stdin: str = "",
expected_output: str = "",
cpu_time_limit: float = 1.0,
memory_limit: int = 262144,
) -> dict:
"""
Execute code using Piston and return result in Judge0-compatible format.
"""
language, version = self._get_piston_language(language_id)
normalized_expected = self._normalize_output(expected_output)
# Determine file extension
ext_map = {
"python": "py", "c": "c", "c++": "cpp", "java": "java",
"javascript": "js", "go": "go", "rust": "rs", "kotlin": "kt",
}
ext = ext_map.get(language, "txt")
filename = f"main.{ext}"
# For Java, extract class name from source
if language == "java":
import re
match = re.search(r'public\s+class\s+(\w+)', source_code)
if match:
filename = f"{match.group(1)}.java"
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.post(
f"{self.base_url}/api/v2/execute",
json={
"language": language,
"version": version,
"files": [
{
"name": filename,
"content": source_code,
}
],
"stdin": stdin,
"run_timeout": int(cpu_time_limit * 1000), # Convert to ms
"compile_timeout": 10000, # 10 seconds for compilation
},
)
response.raise_for_status()
result = response.json()
except httpx.HTTPStatusError as e:
return {
"status": {"id": JudgeStatus.INTERNAL_ERROR, "description": "Internal Error"},
"stdout": "",
"stderr": str(e),
"compile_output": "",
"message": f"Piston API error: {e.response.status_code}",
"time": None,
"memory": None,
}
except Exception as e:
return {
"status": {"id": JudgeStatus.INTERNAL_ERROR, "description": "Internal Error"},
"stdout": "",
"stderr": str(e),
"compile_output": "",
"message": str(e),
"time": None,
"memory": None,
}
# Convert Piston response to Judge0-compatible format
run_result = result.get("run", {})
compile_result = result.get("compile", {})
stdout = run_result.get("stdout", "") or ""
stderr = run_result.get("stderr", "") or ""
compile_output = compile_result.get("output", "") or ""
exit_code = run_result.get("code", 0)
signal = run_result.get("signal")
# Determine status
if compile_result.get("code") is not None and compile_result.get("code") != 0:
# Compilation error
status_id = JudgeStatus.COMPILATION_ERROR
status_desc = "Compilation Error"
elif signal == "SIGKILL":
# Usually means timeout or memory limit
status_id = JudgeStatus.TIME_LIMIT_EXCEEDED
status_desc = "Time Limit Exceeded"
elif signal is not None:
# Runtime error with signal
signal_map = {
"SIGSEGV": JudgeStatus.RUNTIME_ERROR_SIGSEGV,
"SIGFPE": JudgeStatus.RUNTIME_ERROR_SIGFPE,
"SIGABRT": JudgeStatus.RUNTIME_ERROR_SIGABRT,
}
status_id = signal_map.get(signal, JudgeStatus.RUNTIME_ERROR_OTHER)
status_desc = f"Runtime Error ({signal})"
elif exit_code != 0:
# Non-zero exit code
status_id = JudgeStatus.RUNTIME_ERROR_NZEC
status_desc = "Runtime Error (NZEC)"
else:
# Execution successful - compare output
actual_output = self._normalize_output(stdout)
if actual_output == normalized_expected:
status_id = JudgeStatus.ACCEPTED
status_desc = "Accepted"
else:
status_id = JudgeStatus.WRONG_ANSWER
status_desc = "Wrong Answer"
return {
"status": {"id": status_id, "description": status_desc},
"stdout": stdout,
"stderr": stderr,
"compile_output": compile_output,
"message": "",
"time": None, # Piston doesn't provide execution time in same format
"memory": None, # Piston doesn't provide memory usage
}
judge_service = JudgeService()