feat: 新增 /run 接口,在 go-judge 沙箱中编译运行 C/C++

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 16:27:02 +08:00
1 parent a5cfdc1bb6
commit 3c3169bd26
5 files changed
+151 -2

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
import os
import httpx
# go-judge 沙箱地址,docker-compose 里是 code-judge 容器
GOJUDGE_URL = os.getenv("GOJUDGE_URL", "http://code-judge:5050")
# 状态码沿用前端(原 Judge0)的约定
STATUS_ACCEPTED = 3
STATUS_COMPILE_ERROR = 6
STATUS_RUNTIME_ERROR = 11
SECOND = 1_000_000_000 # go-judge 的时间单位是纳秒
MB = 1024 * 1024
OUTPUT_MAX = 64 * 1024
LANGUAGES = {
"c": {
"source": "main.c",
"compile": ["/usr/bin/gcc", "main.c", "-o", "main", "-lm"],
},
"cpp": {
"source": "main.cpp",
"compile": ["/usr/bin/g++", "main.cpp", "-o", "main"],
},
}
STATUS_MESSAGES = {
"Time Limit Exceeded": "运行超时",
"Memory Limit Exceeded": "内存超出限制",
"Output Limit Exceeded": "输出超出限制",
"Signalled": "程序异常终止",
}
class RunError(Exception):
"""沙箱不可用等非用户代码导致的错误"""
def _cmd(args: list[str], stdin: str, cpu: int, memory: int, **extra) -> dict:
return {
"args": args,
"env": ["PATH=/usr/bin:/bin"],
"files": [
{"content": stdin},
{"name": "stdout", "max": OUTPUT_MAX},
{"name": "stderr", "max": OUTPUT_MAX},
],
"cpuLimit": cpu * SECOND,
"clockLimit": cpu * 2 * SECOND,
"memoryLimit": memory * MB,
"procLimit": 50,
"copyOut": ["stdout", "stderr"],
**extra,
}
async def _run(client: httpx.AsyncClient, cmd: dict) -> dict:
try:
response = await client.post("/run", json={"cmd": [cmd]})
response.raise_for_status()
except httpx.HTTPError as e:
raise RunError(f"判题沙箱不可用:{e}")
result = response.json()[0]
if result["status"] == "Internal Error":
raise RunError(result.get("error") or "判题沙箱内部错误")
return result
def _output(result: dict) -> str:
# 与原来 Judge0 的 redirect_stderr_to_stdout 效果接近:stdout 在前,stderr 在后
files = result.get("files", {})
return (files.get("stdout", "") + files.get("stderr", "")).strip()
async def run_code(language: str, code: str, stdin: str) -> dict:
config = LANGUAGES[language]
async with httpx.AsyncClient(base_url=GOJUDGE_URL, timeout=30) as client:
compiled = await _run(
client,
_cmd(
config["compile"],
"",
cpu=10,
memory=512,
copyIn={config["source"]: {"content": code}},
copyOutCached=["main"],
),
)
if compiled["status"] != "Accepted":
return {"status": STATUS_COMPILE_ERROR, "output": _output(compiled)}
file_id = compiled["fileIds"]["main"]
try:
result = await _run(
client,
_cmd(
["main"],
stdin,
cpu=5,
memory=256,
copyIn={"main": {"fileId": file_id}},
),
)
finally:
try:
await client.delete(f"/file/{file_id}")
except httpx.HTTPError:
pass
output = _output(result)
if result["status"] == "Accepted":
return {"status": STATUS_ACCEPTED, "output": output}
message = STATUS_MESSAGES.get(result["status"])
if message:
output = f"{output}\n{message}".strip()
return {"status": STATUS_RUNTIME_ERROR, "output": output}