feat: 新增 /run 接口,在 go-judge 沙箱中编译运行 C/C++
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5cfdc1bb6
commit
3c3169bd26
5 files changed
+151
-2
No files matched your search
@@ -11,10 +11,13 @@ from schemas import (
|
||||
DebugRequest,
|
||||
FormatRequest,
|
||||
FormatResponse,
|
||||
RunRequest,
|
||||
RunResponse,
|
||||
)
|
||||
from database import DatabaseService
|
||||
from debug_runner import DebugError, run_debug
|
||||
from formatter import format_code, FormatError
|
||||
from runner import run_code, RunError
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
@@ -155,6 +158,16 @@ async def format_code_endpoint(request: FormatRequest) -> FormatResponse:
|
||||
return FormatResponse(code=formatted)
|
||||
|
||||
|
||||
@app.post("/run", response_model=RunResponse)
|
||||
async def run_code_endpoint(request: RunRequest) -> RunResponse:
|
||||
"""在 go-judge 沙箱中编译运行 C/C++ 代码"""
|
||||
try:
|
||||
result = await run_code(request.language, request.code, request.stdin)
|
||||
except RunError as e:
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
return RunResponse(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies = [
|
||||
"uvicorn[standard]>=0.38.0",
|
||||
"ruff>=0.15.17",
|
||||
"sqlparse>=0.5.5",
|
||||
"httpx>=0.28.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -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}
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Literal
|
||||
|
||||
|
||||
class PresetCodeCreate(BaseModel):
|
||||
@@ -30,6 +30,7 @@ class AIAnalysisRequest(BaseModel):
|
||||
|
||||
class DebugRequest(BaseModel):
|
||||
"""调试请求模式,用于调试 Python 代码"""
|
||||
|
||||
code: str
|
||||
inputs: List[str]
|
||||
|
||||
@@ -45,3 +46,18 @@ class FormatResponse(BaseModel):
|
||||
"""格式化代码的响应模式"""
|
||||
|
||||
code: str
|
||||
|
||||
|
||||
class RunRequest(BaseModel):
|
||||
"""运行 C/C++ 代码的请求模式"""
|
||||
|
||||
code: str
|
||||
language: Literal["c", "cpp"]
|
||||
stdin: str = ""
|
||||
|
||||
|
||||
class RunResponse(BaseModel):
|
||||
"""运行结果,status 沿用 Judge0 的状态码:3 成功、6 编译错误、11 运行错误"""
|
||||
|
||||
status: int
|
||||
output: str
|
||||
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 5
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
@@ -73,6 +73,7 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
@@ -91,6 +92,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "aiosqlite", specifier = "==0.21" },
|
||||
{ name = "fastapi", specifier = ">=0.119.1" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "openai", specifier = ">=2.6.0" },
|
||||
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||
|
||||
Reference in new issue
Block a user