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,
|
DebugRequest,
|
||||||
FormatRequest,
|
FormatRequest,
|
||||||
FormatResponse,
|
FormatResponse,
|
||||||
|
RunRequest,
|
||||||
|
RunResponse,
|
||||||
)
|
)
|
||||||
from database import DatabaseService
|
from database import DatabaseService
|
||||||
from debug_runner import DebugError, run_debug
|
from debug_runner import DebugError, run_debug
|
||||||
from formatter import format_code, FormatError
|
from formatter import format_code, FormatError
|
||||||
|
from runner import run_code, RunError
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +158,16 @@ async def format_code_endpoint(request: FormatRequest) -> FormatResponse:
|
|||||||
return FormatResponse(code=formatted)
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies = [
|
|||||||
"uvicorn[standard]>=0.38.0",
|
"uvicorn[standard]>=0.38.0",
|
||||||
"ruff>=0.15.17",
|
"ruff>=0.15.17",
|
||||||
"sqlparse>=0.5.5",
|
"sqlparse>=0.5.5",
|
||||||
|
"httpx>=0.28.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[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 pydantic import BaseModel
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any, Literal
|
||||||
|
|
||||||
|
|
||||||
class PresetCodeCreate(BaseModel):
|
class PresetCodeCreate(BaseModel):
|
||||||
@@ -30,6 +30,7 @@ class AIAnalysisRequest(BaseModel):
|
|||||||
|
|
||||||
class DebugRequest(BaseModel):
|
class DebugRequest(BaseModel):
|
||||||
"""调试请求模式,用于调试 Python 代码"""
|
"""调试请求模式,用于调试 Python 代码"""
|
||||||
|
|
||||||
code: str
|
code: str
|
||||||
inputs: List[str]
|
inputs: List[str]
|
||||||
|
|
||||||
@@ -45,3 +46,18 @@ class FormatResponse(BaseModel):
|
|||||||
"""格式化代码的响应模式"""
|
"""格式化代码的响应模式"""
|
||||||
|
|
||||||
code: str
|
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
|
version = 1
|
||||||
revision = 3
|
revision = 5
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -73,6 +73,7 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
@@ -91,6 +92,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiosqlite", specifier = "==0.21" },
|
{ name = "aiosqlite", specifier = "==0.21" },
|
||||||
{ name = "fastapi", specifier = ">=0.119.1" },
|
{ name = "fastapi", specifier = ">=0.119.1" },
|
||||||
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "openai", specifier = ">=2.6.0" },
|
{ name = "openai", specifier = ">=2.6.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.12.3" },
|
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||||
|
|||||||
Reference in new issue
Block a user