浏览代码

# feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流)

yangyi 1 月之前
父节点
当前提交
19e9b95cc8

+ 9 - 0
AGENTS.md

@@ -23,6 +23,15 @@ Spring Boot 3.5 / Java 17 OJ backend template. Root project is `serve` (see `set
 - JWT secret is a hardcoded base64 key in `JwtTokenProvider`, 24h expiry, using the jjwt 0.12 API (`verifyWith`/`parseSignedClaims`).
 - Swagger UI: `/swagger-ui/index.html`.
 
+### 本地代码沙箱(`space.anyi.codesandbox`)
+
+- 沙箱在独立包 `space.anyi.codesandbox`(与 `space.anyi.serve` 平级,不在 serve 包内)。实现未接入 Spring Bean,直接 `new JVMNativeCodeSandBox()` 调用;`CodeSandBox` 是接口,控制器/服务依赖抽象。
+- `CodeContext.maxMemory` 单位为 **KB**(与 `Submission.memoryUsedKb` 一致),拼接为 `-Xmx<kb>k` 传给子 JVM;`maxTime` 单位为毫秒。不要传字节值(会把 256MB 变成 256GB)。
+- 输出判定不是严格 equals:按行归一化,忽略行尾空白(空格/`\t`/`\r`)与末尾空行(`outputMatches`)。
+- 并发上限 `MAX_CONCURRENT`(默认 10),超限 `tryAcquire(5s)` 失败返回 `SE`「系统繁忙」。超时/异常路径由 `destroyForcibly()` 杀子进程,临时目录 `code/<uuid>` 在 `finally` 中删除,不得在早退路径遗漏。
+- 运行时 `javac`/`java` 取 `JAVA_HOME`(回退 `java.home`),需指向含 javac/java 的 **JDK**;环境为 JDK 17 时测试可运行(Gradle toolchain 为 17)。
+- `JVMNativeCodeSandBoxTest` 需真实 JDK/JVM 子进程,不依赖数据库。
+
 ## Coding style
 
 - Controller methods take concrete DTOs for request bodies/params (e.g. `UserDto`, `LoginDto`) and return concrete VOs for responses (e.g. `UserVo`), never raw `User` entities or generic maps.

+ 68 - 9
README.md

@@ -22,21 +22,24 @@
 - **统一响应**:`Response<T>` 通用响应体 + 全局异常处理(参数校验、业务异常)
 - **请求日志**:基于 `CommonsRequestLoggingFilter` 记录请求入参
 - **接口文档**:Swagger UI 在线调试
+- **本地代码沙箱**:基于本地 JVM 进程的判题执行(当前支持 Java),覆盖编译、逐用例执行、超时控制、异步 IO、输出归一化判定与并发限流
 
 ## 项目结构
 
 ```
 serve_template
 ├── sql/postgersql.sql            # 数据库初始化脚本(表结构、索引与触发器)
-├── src/main/java/space/anyi/serve
-│   ├── ServeApplication.java     # 启动类
-│   ├── config                    # SecurityConfig / WebConfig(CORS/日志) / OpenApiConfig
-│   ├── controller                # AuthController(登录注册) / UserController(用户管理)
-│   ├── entity                    # Response(响应体) / PageVo(分页) / auth / user(DTO、VO、实体)
-│   ├── filter                    # JwtAuthenticationFilter(JWT 解析过滤器)
-│   ├── handler                   # GlobalExceptionHandler + security(JWT 工具、异常入口)
-│   ├── mapper                    # MyBatis-Plus Mapper
-│   └── service                   # UserService 接口 + impl 实现
+├── src/main/java/space/anyi
+│   ├── serve                     # 业务工程包
+│   │   ├── ServeApplication.java # 启动类
+│   │   ├── config                # SecurityConfig / WebConfig(CORS/日志) / OpenApiConfig
+│   │   ├── controller            # AuthController(登录注册) / UserController(用户管理)
+│   │   ├── entity                # Response(响应体) / PageVo(分页) / auth / user(DTO、VO、实体)
+│   │   ├── filter                # JwtAuthenticationFilter(JWT 解析过滤器)
+│   │   ├── handler               # GlobalExceptionHandler + security(JWT 工具、异常入口)
+│   │   ├── mapper                # MyBatis-Plus Mapper
+│   │   └── service               # UserService 接口 + impl 实现
+│   └── codesandbox               # 本地JVM代码沙箱(判题执行)
 └── src/main/resources
     ├── application.yaml          # 应用配置
     └── mapper/UserMapper.xml     # Mapper XML
@@ -154,6 +157,62 @@ erDiagram
     contest ||--o{ contest_registration : "报名(contest_id)"
 ```
 
+## 本地代码沙箱
+
+基于本地 JVM 进程实现的判题沙箱(`src/main/java/space/anyi/codesandbox/`),当前仅支持 Java 语言。通过子进程编译并运行提交代码,与宿主服务隔离。
+
+### 核心类
+
+| 类 | 说明 |
+| --- | --- |
+| `CodeSandBox` | 判题沙箱接口,定义 `process(CodeContext)` |
+| `JVMNativeCodeSandBox` | 基于本地 JVM 子进程的实现 |
+| `CodeContext` | 判题上下文(record):`id`、`code`、`language`、`maxTime`、`maxMemory`、`testCases` |
+| `TestCase` | 测试用例(record):`id`、`input`、`output` |
+| `CodeResponse` | 判题结果(record):`id`、`status`、`usedTime`、`usedMemory`、`testCaseResponses`、`message` |
+| `TestCaseResponse` | 单用例结果(record):`id`、`status`、`usedTime`、`usedMemory` |
+
+状态值复用 `Submission` 实体的常量(`STATUS_AC` / `STATUS_WA` / `STATUS_TLE` / `STATUS_MLE` / `STATUS_RE` / `STATUS_CE` / `STATUS_SE`)。
+
+### 执行流程
+
+1. 将提交代码写入临时目录 `code/<uuid>/Main.java`
+2. 调用 `javac -encoding utf8 Main.java` 编译,编译超时(`COMPILE_TIMEOUT_MS`,默认 10s)判 `SE`
+3. 逐用例执行 `java -Xmx<maxMemory>k -Dfile.encoding=utf8 Main`:
+   - stdout / stderr 异步读取、stdin 异步写入(`CompletableFuture` + 守护线程池),主线程不阻塞
+   - `process.waitFor(maxTime, ms)` 超时则 `destroyForcibly()` 并判 `TLE`,防止死循环耗尽资源
+4. `finally` 中清理临时目录,所有返回路径均保证删除
+5. 统计结果:`usedTime` 取各用例最大值,`usedMemory` 取最小值(实际内存统计尚未实现,标注为 `todo`)
+
+### 输出判定
+
+- 按行归一化比较(`outputMatches`):忽略每行**行尾**空白(空格 / `\t` / `\r`)与末尾空行,行首与中间空白敏感
+- 程序输出上限 `MAX_OUTPUT_LENGTH`(1MB 字符),防止恶意输出耗尽内存
+
+### 并发控制
+
+- `Semaphore` 限制最大并发判题数(`MAX_CONCURRENT`,默认 **10**)
+- 达到上限后 `tryAcquire(5s)` 超时,返回 `SE`「系统繁忙」,不排队阻塞线程
+
+### 资源限制约定
+
+- `maxTime`:毫秒,作为 `waitFor` 的进程存活上限
+- `maxMemory`:**KB**(与 `submission.memory_used_kb` 单位一致),作为 `-Xmx` 参数传给子 JVM
+- `CodeContext` 缺省值:`maxTime = 1000ms`、`maxMemory = 256MB(262144 KB)`
+
+### 注意事项
+
+- `javac` / `java` 路径取 `JAVA_HOME`,未设置时回退到当前运行时 `java.home`;需指向 **JDK(非 JRE)**
+- 本次提交尚未接入业务接口(`JudgeController` / `SubmissionService`),沙箱为独立可调用的能力,测试床见 `JVMNativeCodeSandBoxTest`
+
+### 测试
+
+```bash
+JAVA_HOME=/path/to/jdk17 ./gradlew test --tests "space.anyi.codesandbox.JVMNativeCodeSandBoxTest"
+```
+
+覆盖:AC/WA/TLE/RE、多行输出与行尾空白归一化、死循环超时、10 并发、自定义内存上限(`-Xmx<kb>k` 格式)。
+
 ## 主要依赖
 
 | 依赖 | 用途 |

+ 34 - 0
gradlew

@@ -0,0 +1,34 @@
+package space.anyi.codesandbox;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @fileName: CodeContext
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:10 pm
+ * @description: 判题代码上下文对象
+ */
+public record CodeContext(
+        Long id,
+        String code,
+        String language,
+        Long maxTime,
+        Long maxMemory,
+        List<TestCase> testCases
+) {
+    public CodeContext {
+        Objects.requireNonNull(id, "id must not be null");
+        Objects.requireNonNull(code, "code must not be null");
+        Objects.requireNonNull(language, "language must not be null");
+        Objects.requireNonNull(testCases, "testCases must not be null");
+        maxTime = maxTime == null ? 1000L : maxTime;
+        maxMemory = maxMemory == null ? 256 * 1024L : maxMemory;
+    }
+
+    public CodeContext(Long id, String code, String language, List<TestCase> testCases) {
+        this(id, code, language, 1000L, 256 * 1024L, testCases);
+    }
+}

+ 21 - 0
src/main/java/space/anyi/codesandbox/CodeResponse.java

@@ -0,0 +1,21 @@
+package space.anyi.codesandbox;
+
+import java.util.List;
+
+/**
+ * @fileName: CodeResponse
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:09 pm
+ * @description: 判题执行结果
+ */
+public record CodeResponse(
+        Long id,
+        String status,
+        Long usedTime,
+        Long usedMemory,
+        List<TestCaseResponse> testCaseResponses,
+        String message
+) {
+}

+ 13 - 0
src/main/java/space/anyi/codesandbox/CodeSandBox.java

@@ -0,0 +1,13 @@
+package space.anyi.codesandbox;
+
+/**
+ * @fileName: CodeSandBox
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:03 pm
+ * @description: 判题代码沙箱接口
+ */
+public interface CodeSandBox {
+    CodeResponse process(CodeContext codeContext);
+}

+ 374 - 0
src/main/java/space/anyi/codesandbox/JVMNativeCodeSandBox.java

@@ -0,0 +1,374 @@
+package space.anyi.codesandbox;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import space.anyi.serve.entity.contest.Submission;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * @fileName: JVMNativeCodeSandBox
+ * @projectName: serve
+ * @package: space.anyi.codesandbox
+ * @author: yangyi
+ * @date:8/9/2026 7:29 pm
+ * @description: 使用JVM实现判题沙箱
+ */
+public class JVMNativeCodeSandBox implements CodeSandBox {
+    private final static Logger log = LoggerFactory.getLogger(JVMNativeCodeSandBox.class);
+
+    /** 编译超时时间(毫秒) */
+    private static final long COMPILE_TIMEOUT_MS = 10_000L;
+
+    /** 程序输出截断上限(字节),防止恶意输出耗尽内存 */
+    private static final int MAX_OUTPUT_LENGTH = 1024 * 1024;
+
+    /** 异步IO线程池,守护线程避免阻塞JVM退出 */
+    private static final ExecutorService IO_EXECUTOR = Executors.newCachedThreadPool(r -> {
+        Thread thread = new Thread(r, "codesandbox-io");
+        thread.setDaemon(true);
+        return thread;
+    });
+
+    /** 最大并发判题数,受宿主内存约束 */
+    private static final int MAX_CONCURRENT = 10;
+
+    /** 获取并发许可的超时(毫秒),超时返回SE */
+    private static final int CONCURRENCY_ACQUIRE_TIMEOUT_MS = 5_000;
+
+    /** 并发控制信号量 */
+    private static final Semaphore CONCURRENCY_LIMIT = new Semaphore(MAX_CONCURRENT);
+
+    @Override
+    public CodeResponse process(CodeContext codeContext) {
+        //0.并发控制:达到上限时超时返回SE,避免进程爆炸
+        boolean acquired;
+        try {
+            acquired = CONCURRENCY_LIMIT.tryAcquire(CONCURRENCY_ACQUIRE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return getCodeResponse(codeContext.id(), Submission.STATUS_SE, "系统繁忙");
+        }
+        if (!acquired) {
+            return getCodeResponse(codeContext.id(), Submission.STATUS_SE, "系统繁忙");
+        }
+        try {
+            /**
+             * 1.将代码存储到临时文件
+             * 2.编译代码
+             * 3.执行代码
+             * 4.清理临时文件
+             * 5.统计执行结果
+             */
+            //1.将代码存储到临时文件
+            String currentDir = System.getProperty("user.dir");
+            String uuid = UUID.randomUUID().toString();
+            File workDir = new File(currentDir + File.separator + "code" + File.separator + uuid);
+            File codeFile = new File(workDir + File.separator + "Main.java");
+            try {
+                Files.createDirectories(workDir.toPath());
+                Files.createFile(codeFile.toPath());
+                try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(codeFile))) {
+                    bufferedOutputStream.write(codeContext.code().getBytes(StandardCharsets.UTF_8));
+                    bufferedOutputStream.flush();
+                }
+            } catch (IOException e) {
+                log.error(e.getMessage());
+                return getCodeResponse(codeContext.id(), Submission.STATUS_SE, "创建临时目录失败");
+            }
+            try {
+                CodeResponse compileResponse = compile(codeContext, workDir);
+                if (compileResponse != null) {
+                    return compileResponse;
+                }
+                return execute(codeContext, workDir);
+            } finally {
+                //4.清理临时文件
+                try {
+                    clearTempFile(workDir.toPath());
+                } catch (IOException e) {
+                    log.error("删除临时文件失败:{}", e.getMessage());
+                }
+            }
+        } finally {
+            CONCURRENCY_LIMIT.release();
+        }
+    }
+
+    /**
+     * 2.编译代码
+     * @param codeContext 判题上下文
+     * @param workDir 工作目录
+     * @return 编译失败时返回对应的CodeResponse,编译成功返回null
+     */
+    private CodeResponse compile(CodeContext codeContext, File workDir) {
+        Process process = null;
+        CompletableFuture<String> compileOutFuture = null;
+        try {
+            process = new ProcessBuilder(resolveJavacPath(), "-encoding", "utf8", "Main.java")
+                    //重定向错误输出流到标准输出
+                    .redirectErrorStream(true)
+                    //设置工作目录
+                    .directory(workDir)
+                    .start();
+            //异步读取编译输出,防止主线程阻塞
+            InputStream compileInputStream = process.getInputStream();
+            compileOutFuture = CompletableFuture.supplyAsync(() -> readStream(compileInputStream), IO_EXECUTOR);
+            boolean compiled = process.waitFor(COMPILE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            if (!compiled) {
+                process.destroyForcibly();
+                return getCodeResponse(codeContext.id(), Submission.STATUS_SE, "编译超时");
+            }
+            String message = compileOutFuture.get(2, TimeUnit.SECONDS);
+            if (process.exitValue() != 0) {
+                return getCodeResponse(codeContext.id(), Submission.STATUS_CE, message);
+            }
+        } catch (IOException | InterruptedException e) {
+            if (process != null) {
+                process.destroyForcibly();
+            }
+            if (compileOutFuture != null) {
+                compileOutFuture.cancel(true);
+            }
+            log.error(e.getMessage());
+            return getCodeResponse(codeContext.id(), Submission.STATUS_SE, "编译异常:" + e.getMessage());
+        } catch (ExecutionException | TimeoutException e) {
+            if (process != null) {
+                process.destroyForcibly();
+            }
+            if (compileOutFuture != null) {
+                compileOutFuture.cancel(true);
+            }
+            log.error(e.getMessage());
+            return getCodeResponse(codeContext.id(), Submission.STATUS_SE, e.getMessage());
+        }
+        return null;
+    }
+
+    /**
+     * 3.执行代码
+     * @param codeContext 判题上下文
+     * @param workDir 工作目录
+     * @return 判题结果
+     */
+    private CodeResponse execute(CodeContext codeContext, File workDir) {
+        String javaPath = resolveJavaPath();
+        List<TestCaseResponse> testCaseResponses = new ArrayList<>();
+        for (TestCase testCase : codeContext.testCases()) {
+            long startTime = System.nanoTime();
+            Process process = null;
+            CompletableFuture<String> stdoutFuture = null;
+            CompletableFuture<String> stderrFuture = null;
+            CompletableFuture<Void> stdinFuture = null;
+            try {
+                process = new ProcessBuilder(javaPath, "-Xmx" + codeContext.maxMemory() + "k", "-Dfile.encoding=utf8", "Main").directory(workDir).start();
+                //3.1异步读取标准输出,防止子进程大量输出阻塞主线程
+                InputStream stdoutInputStream = process.getInputStream();
+                stdoutFuture = CompletableFuture.supplyAsync(() -> readStream(stdoutInputStream), IO_EXECUTOR);
+                //3.2异步读取错误输出
+                InputStream stderrInputStream = process.getErrorStream();
+                stderrFuture = CompletableFuture.supplyAsync(() -> readStream(stderrInputStream), IO_EXECUTOR);
+                //3.3异步写入标准输入,写入后关闭以通知EOF
+                OutputStream stdinOutputStream = process.getOutputStream();
+                stdinFuture = CompletableFuture.runAsync(() -> writeInputAndClose(stdinOutputStream, testCase.input()), IO_EXECUTOR);
+                //3.4带超时等待进程结束,防止死循环导致资源枯竭
+                boolean finished = process.waitFor(codeContext.maxTime(), TimeUnit.MILLISECONDS);
+                long elapsedMs = (System.nanoTime() - startTime) / 1_000_000;
+                if (!finished) {
+                    //销毁死循环进程
+                    process.destroyForcibly();
+                    String out = stdoutFuture.get(1, TimeUnit.SECONDS);
+                    String err = stderrFuture.get(1, TimeUnit.SECONDS);
+                    stdinFuture.cancel(true);
+                    testCaseResponses.add(new TestCaseResponse(testCase.id(), Submission.STATUS_TLE, elapsedMs, 0L));
+                    return new CodeResponse(codeContext.id(), Submission.STATUS_TLE, elapsedMs, 0L, testCaseResponses, "运行时间超限");
+                }
+                String out = stdoutFuture.get(2, TimeUnit.SECONDS);
+                String err = stderrFuture.get(2, TimeUnit.SECONDS);
+                stdinFuture.get(2, TimeUnit.SECONDS);
+                //3.5实际运行时间与限制时间比较
+                if (elapsedMs > codeContext.maxTime()) {
+                    testCaseResponses.add(new TestCaseResponse(testCase.id(), Submission.STATUS_TLE, elapsedMs, 0L));
+                    return new CodeResponse(codeContext.id(), Submission.STATUS_TLE, elapsedMs, 0L, testCaseResponses, "运行时间超限");
+                }
+                //todo:3.6获取代码的内存使用量
+                //todo:3.7实际内存使用量与限制内存使用量比较
+                if (process.exitValue() == 0) {
+                    //3.8输出与预期进行比较
+                    if (outputMatches(out, testCase.output())) {
+                        testCaseResponses.add(new TestCaseResponse(testCase.id(), Submission.STATUS_AC, elapsedMs, 0L));
+                    } else {
+                        testCaseResponses.add(new TestCaseResponse(testCase.id(), Submission.STATUS_WA, elapsedMs, 0L));
+                        return getCodeResponse(codeContext.id(), Submission.STATUS_WA, "输出与预期不符");
+                    }
+                } else {
+                    //输出为空时退回标准输出,否则展示错误输出
+                    String message = err.isEmpty() ? out : err;
+                    return getCodeResponse(codeContext.id(), Submission.STATUS_RE, message);
+                }
+            } catch (IOException | InterruptedException e) {
+                if (process != null) {
+                    process.destroyForcibly();
+                }
+                cancel(stdoutFuture, stderrFuture, stdinFuture);
+                log.error(e.getMessage());
+                return getCodeResponse(codeContext.id(), Submission.STATUS_SE, e.getMessage());
+            } catch (ExecutionException | TimeoutException e) {
+                if (process != null) {
+                    process.destroyForcibly();
+                }
+                cancel(stdoutFuture, stderrFuture, stdinFuture);
+                log.error(e.getMessage());
+                return getCodeResponse(codeContext.id(), Submission.STATUS_SE, e.getMessage());
+            }
+        }
+        //5.统计执行结果
+        Optional<TestCaseResponse> maxUsedTimeResponse = testCaseResponses.stream().max((v1, v2) -> v1.usedTime().compareTo(v2.usedTime()));
+        Optional<TestCaseResponse> minUsedMemoryResponse = testCaseResponses.stream().min((v1, v2) -> v1.usedMemory().compareTo(v2.usedMemory()));
+        return new CodeResponse(
+                codeContext.id(),
+                Submission.STATUS_AC,
+                maxUsedTimeResponse.orElse(new TestCaseResponse(null, null, codeContext.maxTime(), null)).usedTime(),
+                minUsedMemoryResponse.orElse(new TestCaseResponse(null, null, null, codeContext.maxMemory())).usedMemory(),
+                testCaseResponses,
+                "");
+    }
+
+    /**
+     * 读取输入流为UTF-8字符串,保留换行等全部字符,超过上限即停止读取,防止恶意输出耗尽内存
+     * @param inputStream 输入流
+     * @return 读取到的内容
+     */
+    private String readStream(InputStream inputStream) {
+        StringBuilder builder = new StringBuilder();
+        try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
+            char[] buffer = new char[8192];
+            int len;
+            while (builder.length() < MAX_OUTPUT_LENGTH && (len = reader.read(buffer)) != -1) {
+                int allowed = Math.min(len, MAX_OUTPUT_LENGTH - builder.length());
+                builder.append(buffer, 0, allowed);
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage());
+        }
+        return builder.toString();
+    }
+
+    /**
+     * 写入测试输入并关闭输出流,关闭以通知EOF使按EOF终止的程序正常退出
+     * @param outputStream 进程标准输入
+     * @param input 测试输入
+     */
+    private void writeInputAndClose(OutputStream outputStream, String input) {
+        try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
+            bufferedWriter.write(input + "\r\n");
+            bufferedWriter.flush();
+        } catch (IOException e) {
+            log.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 比较实际输出与预期输出,忽略行尾空白与末尾空行
+     * @param actual 实际输出
+     * @param expected 预期输出
+     * @return 是否匹配
+     */
+    private boolean outputMatches(String actual, String expected) {
+        return normalizeOutput(actual).equals(normalizeOutput(expected));
+    }
+
+    /**
+     * 归一化输出:按行切分,修剪行尾空白,丢弃末尾空行
+     * @param output 原始输出
+     * @return 归一化后的行列表
+     */
+    private List<String> normalizeOutput(String output) {
+        List<String> lines = new ArrayList<>();
+        for (String line : output.split("\n", -1)) {
+            int end = line.length();
+            while (end > 0 && (line.charAt(end - 1) == ' ' || line.charAt(end - 1) == '\t' || line.charAt(end - 1) == '\r')) {
+                end--;
+            }
+            lines.add(line.substring(0, end));
+        }
+        while (!lines.isEmpty() && lines.get(lines.size() - 1).isEmpty()) {
+            lines.remove(lines.size() - 1);
+        }
+        return lines;
+    }
+
+    /**
+     * 取消指定任务
+     * @param futures 任务列表
+     */
+    private void cancel(CompletableFuture<?>... futures) {
+        for (CompletableFuture<?> future : futures) {
+            if (future != null) {
+                future.cancel(true);
+            }
+        }
+    }
+
+    /**
+     * 删除临时目录
+     * @param path
+     * @return
+     * @throws IOException
+     */
+    private boolean clearTempFile(Path path) throws IOException {
+        File workDir = path.toFile();
+        File[] files = workDir.listFiles();
+        if (files == null) {
+            return Files.deleteIfExists(path);
+        }
+        for (File file : files) {
+            if (file.isDirectory()) {
+                clearTempFile(file.toPath());
+            } else {
+                Files.deleteIfExists(file.toPath());
+            }
+        }
+        return Files.deleteIfExists(path);
+    }
+
+    /**
+     *
+     * @param id
+     * @param status
+     * @param message
+     * @return
+     */
+    private CodeResponse getCodeResponse(Long id, String status, String message) {
+        return new CodeResponse(id, status, 0L, 0L, Collections.emptyList(), message);
+    }
+
+    /**
+     * 解析javac路径,优先使用JAVA_HOME,回退到当前运行时
+     * @return
+     */
+    private String resolveJavacPath() {
+        String javaHome = System.getenv("JAVA_HOME");
+        if (javaHome == null || javaHome.isEmpty()) {
+            javaHome = System.getProperty("java.home");
+        }
+        return javaHome + File.separator + "bin" + File.separator + "javac";
+    }
+
+    /**
+     * 解析java路径,优先使用JAVA_HOME,回退到当前运行时
+     * @return
+     */
+    private String resolveJavaPath() {
+        String javaHome = System.getenv("JAVA_HOME");
+        if (javaHome == null || javaHome.isEmpty()) {
+            javaHome = System.getProperty("java.home");
+        }
+        return javaHome + File.separator + "bin" + File.separator + "java";
+    }
+}

+ 16 - 0
src/main/java/space/anyi/codesandbox/TestCase.java

@@ -0,0 +1,16 @@
+package space.anyi.codesandbox;
+
+/**
+ * @fileName: TestCase
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:13 pm
+ * @description: 测试用例
+ */
+public record TestCase(
+        Long id,
+        String input,
+        String output
+) {
+}

+ 13 - 0
src/main/java/space/anyi/codesandbox/TestCaseResponse.java

@@ -0,0 +1,13 @@
+package space.anyi.codesandbox;
+
+/**
+ * @fileName: TestCaseResponse
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:18 pm
+ * @description: 用例测试结果
+ */
+public record TestCaseResponse(Long id,String status,Long usedTime,Long usedMemory) {
+
+}

+ 2 - 0
src/main/java/space/anyi/serve/entity/contest/Submission.java

@@ -26,6 +26,8 @@ public class Submission {
     public static final String STATUS_RE = "RE";
     /** 判题状态:编译失败 */
     public static final String STATUS_CE = "CE";
+    /** 判题状态:系统错误 */
+    public static final String STATUS_SE = "SE";
 
     @TableId(type = IdType.AUTO)
     private Long id;

+ 126 - 0
src/test/java/space/anyi/codesandbox/JVMNativeCodeSandBoxTest.java

@@ -0,0 +1,126 @@
+package space.anyi.codesandbox;
+
+import org.junit.jupiter.api.Test;
+import space.anyi.serve.entity.contest.Submission;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @fileName: JVMNativeCodeSandBoxTest
+ * @projectName: serve
+ * @package: space.anyi.codeSandBox
+ * @author: yangyi
+ * @date:8/9/2026 7:57 pm
+ * @description: TODO
+ */
+class JVMNativeCodeSandBoxTest {
+public static final JVMNativeCodeSandBox jvmNativeCodeSandBox = new JVMNativeCodeSandBox();
+    @Test
+    void process() {
+        String code = "import java.util.Scanner;\n" +
+                "public class Main {\n" +
+                "    public static final Scanner input = new Scanner(System.in);\n" +
+                "    public static void main(String[] args) {\n" +
+                "        long a = input.nextLong();\n" +
+                "        long b = input.nextLong();\n" +
+                "        long c = a + b;\n" +
+                "        System.out.println(c);\n" +
+                "    }\n" +
+                "}";
+        List<TestCase> testCases = List.of(new TestCase(1L,"1 2","3"),new TestCase(2L,"2 2","4"));
+        CodeContext codeContext = new CodeContext(1L, code, "java", 1000L, 256 * 1024L, testCases);
+        CodeResponse codeResponse = jvmNativeCodeSandBox.process(codeContext);
+        System.out.println(codeResponse);
+    }
+
+    @Test
+    void testInfiniteLoopTle() {
+        String code = "public class Main {\n" +
+                "    public static void main(String[] args) {\n" +
+                "        while (true) {}\n" +
+                "    }\n" +
+                "}";
+        List<TestCase> testCases = List.of(new TestCase(1L, "", ""));
+        CodeContext codeContext = new CodeContext(2L, code, "java", 500L, 256 * 1024L, testCases);
+        long start = System.currentTimeMillis();
+        CodeResponse codeResponse = jvmNativeCodeSandBox.process(codeContext);
+        long elapsed = System.currentTimeMillis() - start;
+        assertEquals(Submission.STATUS_TLE, codeResponse.status());
+        assertTrue(elapsed < 5000, "TLE 超时未生效,实际耗时:" + elapsed + "ms");
+    }
+
+    @Test
+    void testMultiLineOutput() {
+        String code = "public class Main {\n" +
+                "    public static void main(String[] args) {\n" +
+                "        System.out.println(1);\n" +
+                "        System.out.println(2);\n" +
+                "    }\n" +
+                "}";
+        List<TestCase> testCases = List.of(
+                new TestCase(1L, "", "1\n2"),
+                new TestCase(2L, "", "1\n2\n"));
+        CodeContext codeContext = new CodeContext(3L, code, "java", 1000L, 256 * 1024L, testCases);
+        CodeResponse codeResponse = jvmNativeCodeSandBox.process(codeContext);
+        assertEquals(Submission.STATUS_AC, codeResponse.status());
+    }
+
+    @Test
+    void testTrailingWhitespaceAndWa() {
+        String code = "public class Main {\n" +
+                "    public static void main(String[] args) {\n" +
+                "        System.out.println(\"3  \\t\");\n" +
+                "    }\n" +
+                "}";
+        CodeResponse ac = jvmNativeCodeSandBox.process(new CodeContext(4L, code, "java", 1000L,
+                256 * 1024L, List.of(new TestCase(1L, "", "3"))));
+        assertEquals(Submission.STATUS_AC, ac.status());
+        CodeResponse wa = jvmNativeCodeSandBox.process(new CodeContext(5L, code, "java", 1000L,
+                256 * 1024L, List.of(new TestCase(1L, "", "9"))));
+        assertEquals(Submission.STATUS_WA, wa.status());
+    }
+
+    @Test
+    void testConcurrentSubmissions() throws Exception {
+        String code = "public class Main {\n" +
+                "    public static void main(String[] args) {\n" +
+                "        System.out.println(1);\n" +
+                "    }\n" +
+                "}";
+        int threadCount = 10;
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+        CountDownLatch startLatch = new CountDownLatch(1);
+        List<Future<CodeResponse>> futures = new ArrayList<>();
+        for (int i = 0; i < threadCount; i++) {
+            final long id = 100L + i;
+            futures.add(executor.submit(() -> {
+                startLatch.await();
+                CodeContext codeContext = new CodeContext(id, code, "java", 1000L, 256 * 1024L,
+                        List.of(new TestCase(1L, "", "1")));
+                return jvmNativeCodeSandBox.process(codeContext);
+            }));
+        }
+        startLatch.countDown();
+        for (Future<CodeResponse> future : futures) {
+            CodeResponse response = future.get(30, TimeUnit.SECONDS);
+            assertEquals(Submission.STATUS_AC, response.status());
+        }
+        executor.shutdown();
+    }
+
+    @Test
+    void testCustomMaxMemory() {
+        String code = "public class Main {\n" +
+                "    public static void main(String[] args) {\n" +
+                "        System.out.println(42);\n" +
+                "    }\n" +
+                "}";
+        CodeResponse codeResponse = jvmNativeCodeSandBox.process(new CodeContext(6L, code, "java", 1000L,
+                64 * 1024L, List.of(new TestCase(1L, "", "42"))));
+        assertEquals(Submission.STATUS_AC, codeResponse.status());
+    }
+}