Spaces:
Sleeping
Sleeping
| # app.py | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel, Field | |
| from typing import Optional | |
| import subprocess | |
| import textwrap | |
| import ast | |
| import resource | |
| import os | |
| app = FastAPI(title="Python Exec Service (PES) on HuggingFace") | |
| # ----------------- 配置 ----------------- | |
| MAX_CODE_LENGTH = 20_000 # 代码最大长度 | |
| MAX_TIMEOUT = 20 # 固定最长 20 秒(由 PES 控制) | |
| MAX_MEMORY_MB = 256 # 子进程最大内存 | |
| MAX_OUTPUT_SIZE = 4 * 1024 * 1024 # 4MB 输出限制 | |
| # ----------------- 请求 / 响应模型 ----------------- | |
| class ExecRequest(BaseModel): | |
| code: str = Field(..., description="要执行的 Python 代码") | |
| stdin: Optional[str] = Field("", description="传给程序的标准输入") | |
| class ExecResponse(BaseModel): | |
| stdout: str | |
| stderr: str | |
| return_code: Optional[int] | |
| timeout: bool | |
| # ----------------- 安全检查:AST 审计 ----------------- | |
| DANGEROUS_NAMES = { | |
| "__import__", "eval", "exec", "open", "compile", | |
| "input", "globals", "locals", "vars", | |
| "os", "sys", "subprocess", "socket", | |
| "shutil", "pathlib" | |
| } | |
| DANGEROUS_MODULES = { | |
| "os", "sys", "subprocess", "socket", "shutil", | |
| "pathlib", "threading", "multiprocessing" | |
| } | |
| class SafeVisitor(ast.NodeVisitor): | |
| def visit_Import(self, node): | |
| for alias in node.names: | |
| if alias.name.split('.')[0] in DANGEROUS_MODULES: | |
| raise ValueError(f"禁止导入模块: {alias.name}") | |
| self.generic_visit(node) | |
| def visit_ImportFrom(self, node): | |
| if node.module and node.module.split('.')[0] in DANGEROUS_MODULES: | |
| raise ValueError(f"禁止 from 导入模块: {node.module}") | |
| self.generic_visit(node) | |
| def visit_Attribute(self, node): | |
| # 禁止访问 __dict__ / __class__ / __globals__ 等双下划线属性 | |
| if isinstance(node.attr, str) and node.attr.startswith("__"): | |
| raise ValueError(f"禁止访问特殊属性: {node.attr}") | |
| self.generic_visit(node) | |
| def visit_Name(self, node): | |
| if node.id in DANGEROUS_NAMES: | |
| raise ValueError(f"禁止使用名称: {node.id}") | |
| self.generic_visit(node) | |
| def static_security_check(code: str): | |
| try: | |
| tree = ast.parse(code, mode="exec") | |
| except SyntaxError as e: | |
| raise ValueError(f"语法错误: {e}") | |
| SafeVisitor().visit(tree) | |
| # ----------------- 资源限制(子进程 preexec_fn) ----------------- | |
| def set_limits(): | |
| # CPU 时间限制(秒)—— hard limit MAX_TIMEOUT | |
| resource.setrlimit(resource.RLIMIT_CPU, (MAX_TIMEOUT, MAX_TIMEOUT)) | |
| # 地址空间限制(内存) | |
| max_bytes = MAX_MEMORY_MB * 1024 * 1024 | |
| resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes)) | |
| # 文件大小限制(防止写超大文件) | |
| resource.setrlimit(resource.RLIMIT_FSIZE, (MAX_OUTPUT_SIZE, MAX_OUTPUT_SIZE)) | |
| # ----------------- 核心执行 API ----------------- | |
| def execute(req: ExecRequest): | |
| # 1. 限制代码长度 | |
| if len(req.code) > MAX_CODE_LENGTH: | |
| raise HTTPException(status_code=400, detail="代码过长,超过限制") | |
| # 2. 去掉多余缩进 | |
| code = textwrap.dedent(req.code) | |
| # 3. 静态安全检查(AST) | |
| try: | |
| static_security_check(code) | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=f"安全检查不通过: {e}") | |
| # 4. 执行:PES 仅提供命令参数,不允许外部控制 timeout | |
| try: | |
| proc = subprocess.run( | |
| ["python", "-c", code], | |
| input=req.stdin.encode("utf-8") if req.stdin else None, | |
| capture_output=True, | |
| timeout=MAX_TIMEOUT, # 固定 20 秒 | |
| preexec_fn=set_limits # 资源限制 | |
| ) | |
| stdout = proc.stdout[:MAX_OUTPUT_SIZE].decode("utf-8", errors="replace") | |
| stderr = proc.stderr[:MAX_OUTPUT_SIZE].decode("utf-8", errors="replace") | |
| return ExecResponse( | |
| stdout=stdout, | |
| stderr=stderr, | |
| return_code=proc.returncode, | |
| timeout=False | |
| ) | |
| except subprocess.TimeoutExpired as e: | |
| # 子进程超时 | |
| stdout = (e.stdout or b"")[:MAX_OUTPUT_SIZE].decode("utf-8", errors="replace") | |
| stderr = (e.stderr or b"")[:MAX_OUTPUT_SIZE].decode("utf-8", errors="replace") | |
| if not stderr: | |
| stderr = "Process timed out" | |
| return ExecResponse( | |
| stdout=stdout, | |
| stderr=stderr, | |
| return_code=None, | |
| timeout=True | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"执行失败: {e}") | |
| # 本地调试用,HF Space 也可以用这个启动命令 | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run("app:app", host="0.0.0.0", port=port) |