246 lines
8.6 KiB
Python
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()
|