From b9849070bb2580aaca3f86d48cbad860d70adf39 Mon Sep 17 00:00:00 2001 From: meowrain Date: Thu, 12 Feb 2026 19:25:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 7 + .../cn/meowrain/aioj/AbstractCodeSandbox.java | 217 +++++++++++ .../cn/meowrain/aioj/CodeSandboxFactory.java | 35 ++ .../cn/meowrain/aioj/DockerCodeSandbox.java | 366 ++++++++++++++++++ .../meowrain/aioj/JavaDockerCodeSandBox.java | 132 ------- .../meowrain/aioj/JavaNativeCodeSandbox.java | 132 ------- .../java/cn/meowrain/aioj/LanguageConfig.java | 107 +++++ .../cn/meowrain/aioj/NativeCodeSandbox.java | 128 ++++++ 8 files changed, 860 insertions(+), 264 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java create mode 100644 src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java create mode 100644 src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java delete mode 100644 src/main/java/cn/meowrain/aioj/JavaDockerCodeSandBox.java delete mode 100644 src/main/java/cn/meowrain/aioj/JavaNativeCodeSandbox.java create mode 100644 src/main/java/cn/meowrain/aioj/LanguageConfig.java create mode 100644 src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6cc8f09 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(del \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\main\\\\java\\\\cn\\\\meowrain\\\\aioj\\\\JavaDockerCodeSandBox.java\")" + ] + } +} diff --git a/src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java b/src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java new file mode 100644 index 0000000..aa87a48 --- /dev/null +++ b/src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java @@ -0,0 +1,217 @@ +package cn.meowrain.aioj; // 包声明 + +import cn.hutool.core.io.FileUtil; // 文件操作工具 +import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; // 执行代码请求 DTO +import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; // 执行代码响应 DTO +import cn.meowrain.aioj.dto.resp.JudgeInfoDTO; // 判题信息 DTO + +import java.io.File; // 文件路径操作 +import java.nio.charset.StandardCharsets; // UTF-8 字符集 +import java.util.*; // 集合工具类 + +/** + * 代码沙箱抽象基类(模板方法模式) + * 定义了执行代码的完整流程骨架,子类只需实现环境相关的钩子方法 + * + * 流程:校验语言 → 保存代码 → 初始化环境 → 编译 → 逐用例运行 → 构建响应 → 清理 + */ +public abstract class AbstractCodeSandbox implements CodeSandbox { + + protected static final long DEFAULT_TIME_LIMIT = 5000L; // 默认运行超时时间(毫秒) + protected static final long DEFAULT_MEMORY_LIMIT_KB = 256 * 1024L; // 默认内存限制 256MB(KB) + protected static final long COMPILE_TIMEOUT_MS = 30_000L; // 编译超时时间 30 秒(毫秒) + + // ==================== 通用执行结果 ==================== + + /** + * 单个用例的运行结果,Docker 和 Native 子类都返回此类型 + */ + protected static class RunResult { + public final boolean timeout; // 是否超时 + public final boolean oomKilled; // 是否被 OOM Killer 杀死 + public final int exitCode; // 进程退出码 + public final String stdout; // 标准输出内容 + public final String stderr; // 标准错误内容 + public final long timeMs; // 执行耗时(毫秒) + public final long memoryKB; // 内存使用量(KB) + + // 构造方法,初始化所有字段 + public RunResult(boolean timeout, boolean oomKilled, int exitCode, + String stdout, String stderr, long timeMs, long memoryKB) { + this.timeout = timeout; // 设置超时标志 + this.oomKilled = oomKilled; // 设置 OOM 标志 + this.exitCode = exitCode; // 设置退出码 + this.stdout = stdout; // 设置标准输出 + this.stderr = stderr; // 设置标准错误 + this.timeMs = timeMs; // 设置耗时 + this.memoryKB = memoryKB; // 设置内存使用量 + } + } + + // ==================== 子类必须实现的钩子方法 ==================== + + /** + * 初始化执行环境(Docker:创建容器 / Native:无操作) + * + * @param codePath 代码所在目录的绝对路径 + * @param config 语言配置 + * @param memoryLimitKB 内存限制(KB) + * @throws Exception 初始化失败时抛出异常 + */ + protected abstract void initEnvironment(String codePath, LanguageConfig config, long memoryLimitKB) throws Exception; + + /** + * 编译代码(Docker:容器内编译 / Native:宿主机编译) + * + * @param codePath 代码所在目录的绝对路径 + * @param config 语言配置 + * @return 编译成功返回 null,失败返回错误信息 + * @throws Exception 编译过程异常 + */ + protected abstract String compile(String codePath, LanguageConfig config) throws Exception; + + /** + * 运行单个测试用例(Docker:容器内执行 / Native:本地进程执行) + * + * @param input 测试用例输入(通过 stdin 传入) + * @param codePath 代码所在目录的绝对路径 + * @param config 语言配置 + * @param timeLimitMs 运行超时时间(毫秒) + * @param memoryLimitKB 内存限制(KB) + * @return 运行结果 + * @throws Exception 运行过程异常 + */ + protected abstract RunResult runCase(String input, String codePath, LanguageConfig config, + long timeLimitMs, long memoryLimitKB) throws Exception; + + /** + * 清理执行环境(Docker:删除容器 / Native:无操作) + * 注意:临时文件的删除由基类统一处理,子类只需清理自己创建的环境资源 + */ + protected abstract void cleanupEnvironment(); + + // ==================== 模板方法:定义执行流程骨架 ==================== + + /** + * 执行用户代码的完整流程(模板方法,不可被子类覆盖) + * + * 流程步骤: + * 1. 校验语言 → 获取 LanguageConfig + * 2. 保存代码到临时目录 + * 3. 初始化执行环境(子类实现) + * 4. 编译(子类实现,如需) + * 5. 逐用例运行(子类实现) + * 6. 构建成功响应 + * 7. 清理环境 + 删除临时文件 + * + * @param request 执行请求 + * @return 执行响应 + */ + @Override + public final ExecuteCodeResponseDTO executeCode(ExecuteCodeRequestDTO request) { + // ---- 解析请求参数 ---- + String code = request.getCode(); // 获取用户代码 + String language = request.getLanguage(); // 获取编程语言 + List inputList = request.getInputList(); // 获取测试用例输入列表 + // 获取时间限制,未设置则使用默认值 + long timeLimit = request.getTimeLimit() != null ? request.getTimeLimit() : DEFAULT_TIME_LIMIT; + // 获取内存限制,未设置则使用默认值 + long memoryLimitKB = request.getMemoryLimit() != null ? request.getMemoryLimit() : DEFAULT_MEMORY_LIMIT_KB; + + if (inputList == null) { // 如果输入列表为 null + inputList = Collections.emptyList(); // 替换为空列表,避免空指针 + } + + ExecuteCodeResponseDTO response = new ExecuteCodeResponseDTO(); // 创建响应对象 + + // ---- 步骤1:校验语言配置 ---- + LanguageConfig langConfig = LanguageConfig.fromLanguage(language); // 根据语言名称查找配置 + if (langConfig == null) { // 不支持的语言 + response.setStatus(3); // 设置状态:运行错误 + response.setMessage("不支持的语言: " + language // 拼接错误信息 + + ",支持: " + LanguageConfig.supportedLanguages()); // 列出支持的语言 + return response; // 直接返回 + } + + // ---- 步骤2:保存代码到临时目录 ---- + String userDir = System.getProperty("user.dir"); // 获取项目根目录 + String globalCodePathName = userDir + File.separator + "tempcode"; // 全局临时代码目录 + if (!FileUtil.exist(globalCodePathName)) { // 如果目录不存在 + FileUtil.mkdir(globalCodePathName); // 创建目录 + } + String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID(); // 用 UUID 生成唯一子目录 + String userCodePath = userCodeParentPath + File.separator + langConfig.getFileName(); // 拼接代码文件完整路径 + FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8); // 将用户代码写入文件 + + try { + // ---- 步骤3:初始化执行环境(子类实现) ---- + initEnvironment(userCodeParentPath, langConfig, memoryLimitKB); + + // ---- 步骤4:编译(子类实现) ---- + if (langConfig.needsCompile()) { // 判断该语言是否需要编译 + String compileError = compile(userCodeParentPath, langConfig); // 调用子类编译方法 + if (compileError != null) { // 编译失败 + response.setStatus(2); // 设置状态:编译错误 + response.setMessage(compileError); // 设置编译错误信息 + return response; // 直接返回 + } + } + + // ---- 步骤5:逐用例运行(子类实现) ---- + List outputList = new ArrayList<>(); // 存储每个用例的输出 + long maxTime = 0; // 记录最大耗时 + long maxMemory = 0; // 记录最大内存使用 + + // 遍历输入列表,如果为空则执行一次无输入的运行 + for (String input : inputList.isEmpty() ? Collections.singletonList("") : inputList) { + // 调用子类的运行方法 + RunResult result = runCase(input, userCodeParentPath, langConfig, timeLimit, memoryLimitKB); + + if (result.timeout) { // 运行超时 + response.setStatus(4); // 设置状态:超时 + response.setMessage("运行超时"); // 设置错误消息 + return response; // 直接返回,跳过剩余用例 + } + + if (result.oomKilled) { // 内存超限 + response.setStatus(5); // 设置状态:内存超限 + response.setMessage("内存超限"); // 设置错误消息 + return response; // 直接返回,跳过剩余用例 + } + + if (result.exitCode != 0) { // 运行错误(非零退出码) + response.setStatus(3); // 设置状态:运行错误 + response.setMessage(result.stderr.isEmpty() // 优先显示 stderr + ? "运行错误,退出码: " + result.exitCode // stderr 为空则显示退出码 + : result.stderr); // 显示 stderr 内容 + return response; // 直接返回,跳过剩余用例 + } + + outputList.add(result.stdout); // 将标准输出加入结果列表 + maxTime = Math.max(maxTime, result.timeMs); // 更新最大耗时 + maxMemory = Math.max(maxMemory, result.memoryKB); // 更新最大内存 + } + + // ---- 步骤6:所有测试用例通过,构建成功响应 ---- + response.setStatus(1); // 设置状态:成功 + response.setMessage("成功"); // 设置成功消息 + response.setOutputList(outputList); // 设置输出列表 + + JudgeInfoDTO judgeInfo = new JudgeInfoDTO(); // 创建判题信息对象 + judgeInfo.setMessage("Accepted"); // 设置判题消息 + judgeInfo.setTime(maxTime); // 设置最大耗时 + judgeInfo.setMemory(maxMemory); // 设置最大内存 + response.setJudgeInfo(judgeInfo); // 设置判题信息到响应 + + } catch (Exception e) { // 捕获所有未预期的异常 + response.setStatus(3); // 设置状态:运行错误 + response.setMessage("系统错误: " + e.getMessage()); // 设置错误消息 + } finally { + // ---- 步骤7:清理(子类环境清理 + 基类删除临时文件) ---- + cleanupEnvironment(); // 子类清理执行环境(如删除 Docker 容器) + FileUtil.del(userCodeParentPath); // 基类统一删除临时代码目录 + } + + return response; // 返回执行响应 + } +} diff --git a/src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java b/src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java new file mode 100644 index 0000000..e17d518 --- /dev/null +++ b/src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java @@ -0,0 +1,35 @@ +package cn.meowrain.aioj; // 包声明 + +/** + * 代码沙箱工厂(工厂模式) + * 根据类型字符串创建对应的 CodeSandbox 实例 + * 调用方无需感知具体实现类 + * + * 使用示例: + * CodeSandbox sandbox = CodeSandboxFactory.create("docker"); + * ExecuteCodeResponseDTO response = sandbox.executeCode(request); + */ +public class CodeSandboxFactory { + + /** + * 根据类型创建代码沙箱实例 + * + * @param type 沙箱类型:"docker" 或 "native" + * @return CodeSandbox 实例 + * @throws IllegalArgumentException 不支持的类型 + */ + public static CodeSandbox create(String type) { + if (type == null) { // 参数为 null + throw new IllegalArgumentException("沙箱类型不能为空"); // 抛出异常 + } + switch (type.toLowerCase()) { // 转小写后匹配 + case "docker": // Docker 沙箱 + return new DockerCodeSandbox(); // 返回 Docker 实现(容器隔离,生产推荐) + case "native": // 本地沙箱 + return new NativeCodeSandbox(); // 返回本地实现(无隔离,开发测试用) + default: // 未知类型 + throw new IllegalArgumentException( // 抛出异常 + "不支持的沙箱类型: " + type + ",支持: docker, native"); // 列出支持的类型 + } + } +} diff --git a/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java b/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java new file mode 100644 index 0000000..9e2aa2f --- /dev/null +++ b/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java @@ -0,0 +1,366 @@ +package cn.meowrain.aioj; // 包声明 + +// ==================== Hutool 工具类导入 ==================== +import cn.hutool.core.io.FileUtil; // 文件操作工具(写入 input.txt) +import cn.hutool.core.io.resource.ResourceUtil; // 资源文件读取工具(测试用) +import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; // 执行代码请求 DTO +import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; // 执行代码响应 DTO + +// ==================== Docker Java 客户端导入 ==================== +import com.github.dockerjava.api.DockerClient; // Docker 客户端接口 +import com.github.dockerjava.api.async.ResultCallback; // 异步结果回调 +import com.github.dockerjava.api.command.CreateContainerResponse; // 创建容器响应 +import com.github.dockerjava.api.command.ExecCreateCmdResponse; // 创建 exec 响应 +import com.github.dockerjava.api.command.InspectContainerResponse; // 容器检查响应 +import com.github.dockerjava.api.command.InspectExecResponse; // exec 检查响应 +import com.github.dockerjava.api.command.PullImageResultCallback; // 拉取镜像回调 +import com.github.dockerjava.api.model.*; // Docker 模型类 +import com.github.dockerjava.core.DefaultDockerClientConfig; // Docker 客户端默认配置 +import com.github.dockerjava.core.DockerClientConfig; // Docker 客户端配置接口 +import com.github.dockerjava.core.DockerClientImpl; // Docker 客户端实现 +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; // Apache HttpClient5 传输层 + +// ==================== Java 标准库导入 ==================== +import java.io.File; // 文件路径操作 +import java.nio.charset.StandardCharsets; // UTF-8 字符集 +import java.time.Duration; // 时间间隔 +import java.util.*; // 集合工具类 +import java.util.concurrent.ConcurrentHashMap; // 线程安全的 HashMap +import java.util.concurrent.TimeUnit; // 时间单位 + +/** + * Docker 代码沙箱实现(继承模板方法基类) + * 所有用户代码在 Docker 容器内隔离执行,保障宿主机安全 + * 支持多语言:Java、Go、Python、JavaScript + */ +public class DockerCodeSandbox extends AbstractCodeSandbox { + + private static final String CONTAINER_WORK_DIR = "/app"; // 容器内的工作目录 + + private static volatile DockerClient dockerClient; // Docker 客户端单例(volatile 保证多线程可见性) + /** 记录已确认存在的镜像名称集合,避免每次都查询 Docker */ + private static final Set readyImages = ConcurrentHashMap.newKeySet(); // 线程安全的 Set + + private String containerId; // 当前执行使用的容器 ID(每次 executeCode 期间有效) + + /** + * 测试入口方法 + */ + public static void main(String[] args) { + DockerCodeSandbox sandbox = new DockerCodeSandbox(); // 创建 Docker 沙箱实例 + + // 从资源文件中读取测试代码 + String code = ResourceUtil.readStr("testCode.multiargs/Main.java", StandardCharsets.UTF_8); + // 构建执行请求 + ExecuteCodeRequestDTO request = ExecuteCodeRequestDTO.builder() + .code(code) // 设置用户代码 + .language("java") // 设置编程语言 + .inputList(Arrays.asList("1 2", "3 4")) // 设置测试用例输入列表 + .build(); // 构建请求对象 + + ExecuteCodeResponseDTO response = sandbox.executeCode(request); // 执行代码 + System.out.println("状态: " + response.getStatus()); // 打印执行状态 + System.out.println("消息: " + response.getMessage()); // 打印执行消息 + System.out.println("输出: " + response.getOutputList()); // 打印输出列表 + if (response.getJudgeInfo() != null) { // 如果有判题信息 + System.out.println("耗时: " + response.getJudgeInfo().getTime() + "ms"); // 打印耗时 + System.out.println("内存: " + response.getJudgeInfo().getMemory() + "KB"); // 打印内存 + } + System.exit(0); // 强制退出 JVM,防止 Docker 客户端线程阻止退出 + } + + // ==================== 实现模板方法的钩子 ==================== + + /** + * 初始化 Docker 执行环境:确保镜像存在 + 创建并启动容器 + */ + @Override + protected void initEnvironment(String codePath, LanguageConfig config, long memoryLimitKB) throws Exception { + ensureImageExists(config.getImage()); // 检查并拉取镜像 + containerId = createContainer(codePath, config.getImage(), memoryLimitKB); // 创建并启动容器 + } + + /** + * 在容器内编译代码 + * + * @return 编译成功返回 null,失败返回错误信息 + */ + @Override + protected String compile(String codePath, LanguageConfig config) throws Exception { + // 在容器内执行编译命令 + ExecResult result = execInContainer(containerId, COMPILE_TIMEOUT_MS, + config.getEnvVars(), config.getCompileCmd()); // 使用语言配置的编译命令和环境变量 + + if (result.timeout) { // 编译超时 + return "编译超时"; // 返回超时错误 + } + if (result.exitCode != 0) { // 编译失败 + return "编译失败:\n" + (result.stderr.isEmpty() // 优先返回 stderr + ? result.stdout : result.stderr); // stderr 为空则返回 stdout + } + return null; // 编译成功 + } + + /** + * 在容器内运行单个测试用例 + * 将输入写入 input.txt,通过管道传给程序的 stdin + */ + @Override + protected RunResult runCase(String input, String codePath, LanguageConfig config, + long timeLimitMs, long memoryLimitKB) throws Exception { + // 将测试输入写入 input.txt(容器内通过 cat 读取) + String inputFilePath = codePath + File.separator + "input.txt"; // input.txt 宿主机路径 + FileUtil.writeString(input != null ? input : "", inputFilePath, StandardCharsets.UTF_8); // 写入输入内容 + + // 计算 JVM 堆内存限制(容器内存的 80%,最低 16MB) + long xmxMB = Math.max(16, (memoryLimitKB / 1024) * 80 / 100); + String[] runCmd = config.getRunCmd(xmxMB); // 获取运行命令(替换内存占位符) + + // 构建容器内执行命令:sh -c "cat /app/input.txt | <运行命令>" + String runCmdStr = String.join(" ", runCmd); // 将运行命令数组拼接为字符串 + String[] execCmd = new String[]{"sh", "-c", "cat /app/input.txt | " + runCmdStr}; // 管道传入 stdin + + // 在容器内执行 + ExecResult result = execInContainer(containerId, timeLimitMs, null, execCmd); + + // 将 Docker ExecResult 转换为通用 RunResult + return new RunResult(result.timeout, result.oomKilled, result.exitCode, + result.stdout, result.stderr, result.timeMs, result.memoryKB); + } + + /** + * 清理 Docker 容器 + */ + @Override + protected void cleanupEnvironment() { + cleanupContainer(containerId); // 停止并删除容器 + containerId = null; // 重置容器 ID + } + + // ==================== Docker 客户端管理 ==================== + + /** + * 懒加载获取 Docker 客户端单例(synchronized 保证线程安全) + */ + private static synchronized DockerClient getDockerClient() { + if (dockerClient == null) { // 如果客户端尚未初始化 + // 使用默认配置(Linux 上自动连接 unix:///var/run/docker.sock) + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + // 创建 Apache HttpClient5 传输层 + ApacheDockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(config.getDockerHost()) // 设置 Docker 守护进程地址 + .sslConfig(config.getSSLConfig()) // 设置 SSL 配置 + .maxConnections(10) // 最大连接数 + .connectionTimeout(Duration.ofSeconds(30)) // 连接超时 30 秒 + .responseTimeout(Duration.ofSeconds(45)) // 响应超时 45 秒 + .build(); // 构建 HTTP 客户端 + dockerClient = DockerClientImpl.getInstance(config, httpClient); // 创建 Docker 客户端实例 + } + return dockerClient; // 返回客户端单例 + } + + /** + * 确保指定的 Docker 镜像存在,不存在则自动拉取 + * 使用双重检查锁定避免并发重复拉取 + */ + private static void ensureImageExists(String image) { + if (readyImages.contains(image)) return; // 快速路径:已确认存在 + synchronized (DockerCodeSandbox.class) { // 加锁防止并发拉取 + if (readyImages.contains(image)) return; // 双重检查 + DockerClient client = getDockerClient(); // 获取客户端 + List images = client.listImagesCmd() // 查询本地镜像 + .withImageNameFilter(image) // 按名称过滤 + .exec(); // 执行查询 + if (images.isEmpty()) { // 本地不存在 + System.out.println("拉取镜像: " + image); // 打印提示 + try { + client.pullImageCmd(image) // 拉取镜像 + .exec(new PullImageResultCallback()) // 设置回调 + .awaitCompletion(5, TimeUnit.MINUTES); // 等待最多 5 分钟 + } catch (InterruptedException e) { // 被中断 + Thread.currentThread().interrupt(); // 恢复中断标志 + throw new RuntimeException("镜像拉取被中断: " + image, e); // 抛出异常 + } + System.out.println("镜像拉取完成: " + image); // 打印完成提示 + } + readyImages.add(image); // 标记已就绪 + } + } + + // ==================== 容器生命周期 ==================== + + /** + * 创建并启动一个 Docker 容器 + */ + private String createContainer(String hostCodePath, String image, long memoryLimitKB) { + DockerClient client = getDockerClient(); // 获取客户端 + long memoryBytes = memoryLimitKB * 1024L; // KB 转字节 + + // 配置宿主机资源限制和安全选项 + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(hostCodePath, new Volume(CONTAINER_WORK_DIR))) // 挂载代码目录到 /app + .withMemory(memoryBytes) // 内存硬限制 + .withMemorySwap(memoryBytes) // 禁用 swap + .withCpuCount(1L) // 限制 1 个 CPU + .withNetworkMode("none") // 禁止网络 + .withPidsLimit(100L) // 防 fork 炸弹 + .withSecurityOpts(Collections.singletonList("no-new-privileges")); // 禁止提权 + + // 创建容器 + CreateContainerResponse container = client.createContainerCmd(image) // 指定镜像 + .withHostConfig(hostConfig) // 应用配置 + .withWorkingDir(CONTAINER_WORK_DIR) // 工作目录 /app + .withCmd("tail", "-f", "/dev/null") // 保持容器运行 + .withAttachStdin(false) // 不附加 stdin + .withAttachStdout(false) // 不附加 stdout + .withAttachStderr(false) // 不附加 stderr + .withTty(false) // 不分配伪终端 + .exec(); // 执行创建 + + String id = container.getId(); // 获取容器 ID + client.startContainerCmd(id).exec(); // 启动容器 + return id; // 返回容器 ID + } + + /** + * 清理容器:先停止再强制删除 + */ + private void cleanupContainer(String containerId) { + if (containerId == null) return; // 为空则跳过 + DockerClient client = getDockerClient(); // 获取客户端 + try { + client.stopContainerCmd(containerId).withTimeout(1).exec(); // 停止(1 秒优雅关闭) + } catch (Exception ignored) { // 忽略(可能已停止) + } + try { + client.removeContainerCmd(containerId).withForce(true).exec(); // 强制删除 + } catch (Exception ignored) { // 忽略(最大努力) + } + } + + // ==================== 容器内执行 ==================== + + /** + * Docker exec 执行结果(内部使用,在钩子方法中转换为通用 RunResult) + */ + private static class ExecResult { + final boolean timeout; // 是否超时 + final boolean oomKilled; // 是否 OOM + final int exitCode; // 退出码 + final String stdout; // 标准输出 + final String stderr; // 标准错误 + final long timeMs; // 耗时(毫秒) + final long memoryKB; // 内存(KB) + + ExecResult(boolean timeout, boolean oomKilled, int exitCode, + String stdout, String stderr, long timeMs, long memoryKB) { + this.timeout = timeout; + this.oomKilled = oomKilled; + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + this.timeMs = timeMs; + this.memoryKB = memoryKB; + } + } + + /** + * 在容器内执行命令(docker exec) + */ + private ExecResult execInContainer(String containerId, long timeLimitMs, + String[] envVars, String... command) { + DockerClient client = getDockerClient(); // 获取客户端 + + // 1. 创建 exec 实例 + var execCreate = client.execCreateCmd(containerId) // 在容器内创建 exec + .withCmd(command) // 设置命令 + .withAttachStdout(true) // 附加 stdout + .withAttachStderr(true); // 附加 stderr + if (envVars != null) { // 如果有环境变量 + execCreate.withEnv(Arrays.asList(envVars)); // 设置环境变量 + } + ExecCreateCmdResponse exec = execCreate.exec(); // 执行创建 + String execId = exec.getId(); // 获取 exec ID + + // 2. 收集输出 + StringBuilder stdoutBuilder = new StringBuilder(); // stdout 收集器 + StringBuilder stderrBuilder = new StringBuilder(); // stderr 收集器 + + ResultCallback.Adapter callback = new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { // 收到数据帧 + String payload = new String(frame.getPayload(), StandardCharsets.UTF_8); // 转字符串 + if (frame.getStreamType() == StreamType.STDOUT) { // stdout 帧 + stdoutBuilder.append(payload); // 追加 + } else if (frame.getStreamType() == StreamType.STDERR) { // stderr 帧 + stderrBuilder.append(payload); // 追加 + } + } + }; + + // 3. 启动 exec 并计时 + long startTime = System.currentTimeMillis(); // 记录开始时间 + boolean completed; // 是否完成 + try { + client.execStartCmd(execId).exec(callback); // 启动 exec + completed = callback.awaitCompletion(timeLimitMs, TimeUnit.MILLISECONDS); // 等待完成或超时 + } catch (InterruptedException e) { // 被中断 + Thread.currentThread().interrupt(); // 恢复中断 + completed = false; // 标记未完成 + } + long elapsed = System.currentTimeMillis() - startTime; // 计算耗时 + + // 4. 超时则杀容器 + if (!completed) { // 超时未完成 + try { client.killContainerCmd(containerId).exec(); } catch (Exception ignored) {} // 杀容器 + return new ExecResult(true, false, -1, "", "", elapsed, 0); // 返回超时结果 + } + + // 5. 获取退出码 + InspectExecResponse execInspect = client.inspectExecCmd(execId).exec(); // 检查 exec + int exitCode = execInspect.getExitCodeLong() != null // 获取退出码 + ? execInspect.getExitCodeLong().intValue() : -1; + + // 6. 获取内存快照 + long memoryKB = getContainerMemoryKB(containerId); // 查询内存 + + // 7. 检测 OOM + boolean oomKilled = false; // 默认非 OOM + if (exitCode == 137) { // 被 SIGKILL + try { + InspectContainerResponse info = client.inspectContainerCmd(containerId).exec(); // 检查容器 + if (info.getState() != null && Boolean.TRUE.equals(info.getState().getOOMKilled())) { // 确认 OOM + oomKilled = true; // 是 OOM + } + } catch (Exception ignored) {} + } + + return new ExecResult(false, oomKilled, exitCode, + stdoutBuilder.toString().trim(), // stdout(去空白) + stderrBuilder.toString().trim(), // stderr(去空白) + elapsed, memoryKB); + } + + /** + * 获取容器内存使用量快照(KB) + */ + private long getContainerMemoryKB(String containerId) { + DockerClient client = getDockerClient(); // 获取客户端 + final long[] usage = {0}; // 结果容器 + try { + ResultCallback.Adapter cb = new ResultCallback.Adapter() { + @Override + public void onNext(Statistics stats) { // 收到 stats + if (stats.getMemoryStats() != null && stats.getMemoryStats().getUsage() != null) { // 有内存数据 + usage[0] = stats.getMemoryStats().getUsage() / 1024; // 字节转 KB + } + try { close(); } catch (Exception ignored) {} // 关闭,只需一次 + } + }; + client.statsCmd(containerId).withNoStream(true).exec(cb); // 非流式查询 + cb.awaitCompletion(5, TimeUnit.SECONDS); // 等待最多 5 秒 + } catch (Exception ignored) { // 忽略异常 + } + return usage[0]; // 返回内存使用量 + } +} diff --git a/src/main/java/cn/meowrain/aioj/JavaDockerCodeSandBox.java b/src/main/java/cn/meowrain/aioj/JavaDockerCodeSandBox.java deleted file mode 100644 index 187b2ae..0000000 --- a/src/main/java/cn/meowrain/aioj/JavaDockerCodeSandBox.java +++ /dev/null @@ -1,132 +0,0 @@ -package cn.meowrain.aioj; - -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.io.resource.ResourceUtil; -import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; -import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; -import cn.meowrain.aioj.dto.resp.JudgeInfoDTO; -import cn.meowrain.aioj.utils.ProcessUtil; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -public class JavaDockerCodeSandBox implements CodeSandbox { - - private static final long DEFAULT_TIME_LIMIT = 5000L; - - public static void main(String[] args) { - JavaDockerCodeSandBox javaDockerCodeSandBox = new JavaDockerCodeSandBox(); - - String code = ResourceUtil.readStr("testCode.multiargs/Main.java", StandardCharsets.UTF_8); - - String language = "java"; - ExecuteCodeRequestDTO executeCodeRequestDTO = ExecuteCodeRequestDTO.builder() - .code(code) - .language(language) - .inputList(Arrays.asList("1,2","1,3")) - .build(); - - ExecuteCodeResponseDTO response = javaDockerCodeSandBox.executeCode(executeCodeRequestDTO); - System.out.println("状态: " + response.getStatus()); - System.out.println("消息: " + response.getMessage()); - System.out.println("输出: " + response.getOutputList()); - if (response.getJudgeInfo() != null) { - System.out.println("判题信息: " + response.getJudgeInfo().getMessage()); - System.out.println("耗时: " + response.getJudgeInfo().getTime() + "ms"); - System.out.println("内存: " + response.getJudgeInfo().getMemory() + "KB"); - } - System.exit(0); - } - - @Override - public ExecuteCodeResponseDTO executeCode(ExecuteCodeRequestDTO request) { - String code = request.getCode(); - List inputList = request.getInputList(); - long timeLimit = request.getTimeLimit() != null ? request.getTimeLimit() : DEFAULT_TIME_LIMIT; - - if (inputList == null) { - inputList = Collections.emptyList(); - } - - ExecuteCodeResponseDTO response = new ExecuteCodeResponseDTO(); - - // 1. 保存用户代码到临时目录 - String userDir = System.getProperty("user.dir"); - String globalCodePathName = userDir + File.separator + "tempcode"; - if (!FileUtil.exist(globalCodePathName)) { - FileUtil.mkdir(globalCodePathName); - } - - String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID(); - String userCodePath = userCodeParentPath + File.separator + "Main.java"; - File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8); - - try { - // 2. 编译 - String compileError = ProcessUtil.compile(userCodeFile); - if (compileError != null) { - response.setStatus(2); - response.setMessage(compileError); - return response; - } - - // 3. 逐个用例执行 - List outputList = new ArrayList<>(); - long maxTime = 0; - long maxMemory = 0; - - for (String input : inputList.isEmpty() ? Collections.singletonList("") : inputList) { - // 构建命令:java -cp Main - // inputList 中每个元素按空格拆分为多个命令行参数 - List runCommand = new ArrayList<>(Arrays.asList("java", "-Xmx256m", "-cp", userCodeParentPath, "Main")); - if (input != null && !input.trim().isEmpty()) { - runCommand.addAll(Arrays.asList(input.trim().split("\\s+"))); - } - ProcessUtil.ExecuteResult result = ProcessUtil.run(runCommand, null, timeLimit); - - if (result.isTimeout()) { - response.setStatus(4); - response.setMessage("运行超时"); - return response; - } - - if (result.getExitCode() != 0) { - response.setStatus(3); - response.setMessage(result.getStderr().isEmpty() - ? "运行错误,退出码: " + result.getExitCode() - : result.getStderr()); - return response; - } - - outputList.add(result.getStdout()); - maxTime = Math.max(maxTime, result.getTime()); - maxMemory = Math.max(maxMemory, result.getMemory()); - } - - // 4. 全部执行成功 - response.setStatus(1); - response.setMessage("成功"); - response.setOutputList(outputList); - - JudgeInfoDTO judgeInfo = new JudgeInfoDTO(); - judgeInfo.setMessage("Accepted"); - judgeInfo.setTime(maxTime); - judgeInfo.setMemory(maxMemory); - response.setJudgeInfo(judgeInfo); - - } catch (Exception e) { - response.setStatus(3); - response.setMessage("系统错误: " + e.getMessage()); - } finally { - // 5. 清理临时文件 - FileUtil.del(userCodeParentPath); - } - - return response; - } -} diff --git a/src/main/java/cn/meowrain/aioj/JavaNativeCodeSandbox.java b/src/main/java/cn/meowrain/aioj/JavaNativeCodeSandbox.java deleted file mode 100644 index ce60103..0000000 --- a/src/main/java/cn/meowrain/aioj/JavaNativeCodeSandbox.java +++ /dev/null @@ -1,132 +0,0 @@ -package cn.meowrain.aioj; - -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.io.resource.ResourceUtil; -import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; -import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; -import cn.meowrain.aioj.dto.resp.JudgeInfoDTO; -import cn.meowrain.aioj.utils.ProcessUtil; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -public class JavaNativeCodeSandbox implements CodeSandbox { - - private static final long DEFAULT_TIME_LIMIT = 5000L; - - public static void main(String[] args) { - JavaNativeCodeSandbox javaNativeCodeSandbox = new JavaNativeCodeSandbox(); - - String code = ResourceUtil.readStr("testCode.multiargs/Main.java", StandardCharsets.UTF_8); - - String language = "java"; - ExecuteCodeRequestDTO executeCodeRequestDTO = ExecuteCodeRequestDTO.builder() - .code(code) - .language(language) - .inputList(Arrays.asList("1,2","1,3")) - .build(); - - ExecuteCodeResponseDTO response = javaNativeCodeSandbox.executeCode(executeCodeRequestDTO); - System.out.println("状态: " + response.getStatus()); - System.out.println("消息: " + response.getMessage()); - System.out.println("输出: " + response.getOutputList()); - if (response.getJudgeInfo() != null) { - System.out.println("判题信息: " + response.getJudgeInfo().getMessage()); - System.out.println("耗时: " + response.getJudgeInfo().getTime() + "ms"); - System.out.println("内存: " + response.getJudgeInfo().getMemory() + "KB"); - } - System.exit(0); - } - - @Override - public ExecuteCodeResponseDTO executeCode(ExecuteCodeRequestDTO request) { - String code = request.getCode(); - List inputList = request.getInputList(); - long timeLimit = request.getTimeLimit() != null ? request.getTimeLimit() : DEFAULT_TIME_LIMIT; - - if (inputList == null) { - inputList = Collections.emptyList(); - } - - ExecuteCodeResponseDTO response = new ExecuteCodeResponseDTO(); - - // 1. 保存用户代码到临时目录 - String userDir = System.getProperty("user.dir"); - String globalCodePathName = userDir + File.separator + "tempcode"; - if (!FileUtil.exist(globalCodePathName)) { - FileUtil.mkdir(globalCodePathName); - } - - String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID(); - String userCodePath = userCodeParentPath + File.separator + "Main.java"; - File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8); - - try { - // 2. 编译 - String compileError = ProcessUtil.compile(userCodeFile); - if (compileError != null) { - response.setStatus(2); - response.setMessage(compileError); - return response; - } - - // 3. 逐个用例执行 - List outputList = new ArrayList<>(); - long maxTime = 0; - long maxMemory = 0; - - for (String input : inputList.isEmpty() ? Collections.singletonList("") : inputList) { - // 构建命令:java -cp Main - // inputList 中每个元素按空格拆分为多个命令行参数 - List runCommand = new ArrayList<>(Arrays.asList("java", "-Xmx256m", "-cp", userCodeParentPath, "Main")); - if (input != null && !input.trim().isEmpty()) { - runCommand.addAll(Arrays.asList(input.trim().split("\\s+"))); - } - ProcessUtil.ExecuteResult result = ProcessUtil.run(runCommand, null, timeLimit); - - if (result.isTimeout()) { - response.setStatus(4); - response.setMessage("运行超时"); - return response; - } - - if (result.getExitCode() != 0) { - response.setStatus(3); - response.setMessage(result.getStderr().isEmpty() - ? "运行错误,退出码: " + result.getExitCode() - : result.getStderr()); - return response; - } - - outputList.add(result.getStdout()); - maxTime = Math.max(maxTime, result.getTime()); - maxMemory = Math.max(maxMemory, result.getMemory()); - } - - // 4. 全部执行成功 - response.setStatus(1); - response.setMessage("成功"); - response.setOutputList(outputList); - - JudgeInfoDTO judgeInfo = new JudgeInfoDTO(); - judgeInfo.setMessage("Accepted"); - judgeInfo.setTime(maxTime); - judgeInfo.setMemory(maxMemory); - response.setJudgeInfo(judgeInfo); - - } catch (Exception e) { - response.setStatus(3); - response.setMessage("系统错误: " + e.getMessage()); - } finally { - // 5. 清理临时文件 - FileUtil.del(userCodeParentPath); - } - - return response; - } -} diff --git a/src/main/java/cn/meowrain/aioj/LanguageConfig.java b/src/main/java/cn/meowrain/aioj/LanguageConfig.java new file mode 100644 index 0000000..76336d2 --- /dev/null +++ b/src/main/java/cn/meowrain/aioj/LanguageConfig.java @@ -0,0 +1,107 @@ +package cn.meowrain.aioj; // 包声明 + +import java.util.Arrays; // 导入数组工具类 + +/** + * 语言配置枚举,定义每种语言的 Docker 镜像、文件名、编译命令、运行命令 + */ +public enum LanguageConfig { + + // Java 语言配置:使用 openjdk:17-slim 镜像,入口文件 Main.java + JAVA("java", "openjdk:17-slim", "Main.java", + new String[]{"javac", "-encoding", "UTF-8", "/app/Main.java"}, // 编译命令:javac 编译源文件 + new String[]{"java", "-cp", "/app", "Main"}, // 运行命令:java 执行主类 + null), // 无额外环境变量 + + // Go 语言配置:使用 golang:1.22-alpine 镜像,入口文件 main.go + GOLANG("go", "golang:1.22-alpine", "main.go", + new String[]{"go", "build", "-o", "/app/main", "/app/main.go"}, // 编译命令:go build 编译为二进制 + new String[]{"/app/main"}, // 运行命令:直接执行编译后的二进制 + new String[]{"CGO_ENABLED=0", "GOPROXY=https://goproxy.cn,direct"}), // 环境变量:禁用 CGO + 国内代理 + + // Python 语言配置:使用 python:3.12-slim 镜像,入口文件 main.py + PYTHON("python", "python:3.12-slim", "main.py", + null, // 无需编译 + new String[]{"python3", "/app/main.py"}, // 运行命令:python3 解释执行 + null), // 无额外环境变量 + + // JavaScript 语言配置:使用 node:20-slim 镜像,入口文件 main.js + JAVASCRIPT("javascript", "node:20-slim", "main.js", + null, // 无需编译 + new String[]{"node", "/app/main.js"}, // 运行命令:node 解释执行 + null); // 无额外环境变量 + + private final String language; // 语言标识,用于匹配请求中的 language 字段 + private final String image; // Docker 镜像名称 + private final String fileName; // 用户代码的入口文件名 + private final String[] compileCmd; // 编译命令数组,null 表示无需编译(解释型语言) + private final String[] runCmd; // 运行命令数组 + private final String[] envVars; // 编译时需要的额外环境变量,null 表示无 + + // 枚举构造方法,初始化所有字段 + LanguageConfig(String language, String image, String fileName, + String[] compileCmd, String[] runCmd, String[] envVars) { + this.language = language; // 设置语言标识 + this.image = image; // 设置 Docker 镜像 + this.fileName = fileName; // 设置入口文件名 + this.compileCmd = compileCmd; // 设置编译命令 + this.runCmd = runCmd; // 设置运行命令 + this.envVars = envVars; // 设置环境变量 + } + + /** + * 根据语言名称查找对应的配置,忽略大小写 + * + * @param lang 语言名称(如 "java"、"python") + * @return 对应的 LanguageConfig,找不到返回 null + */ + public static LanguageConfig fromLanguage(String lang) { + for (LanguageConfig config : values()) { // 遍历所有枚举值 + if (config.language.equalsIgnoreCase(lang)) { // 忽略大小写比较 + return config; // 匹配成功,返回配置 + } + } + return null; // 未找到匹配的语言配置 + } + + public String getLanguage() { return language; } // 获取语言标识 + public String getImage() { return image; } // 获取 Docker 镜像名称 + public String getFileName() { return fileName; } // 获取入口文件名 + public String[] getCompileCmd() { return compileCmd; } // 获取编译命令 + public String[] getEnvVars() { return envVars; } // 获取环境变量 + + /** + * 获取运行命令,将模板中的 {MEM} 占位符替换为实际内存限制值 + * + * @param xmxMB JVM 最大堆内存(MB),仅对 Java 生效 + * @return 替换后的运行命令数组 + */ + public String[] getRunCmd(long xmxMB) { + String[] cmd = Arrays.copyOf(runCmd, runCmd.length); // 拷贝一份,避免修改原数组 + for (int i = 0; i < cmd.length; i++) { // 遍历命令中的每个部分 + cmd[i] = cmd[i].replace("{MEM}", String.valueOf(xmxMB)); // 替换内存占位符 + } + return cmd; // 返回替换后的命令数组 + } + + /** + * 判断该语言是否需要编译步骤 + * + * @return true 表示需要编译(如 Java、Go),false 表示不需要(如 Python、JS) + */ + public boolean needsCompile() { return compileCmd != null; } + + /** + * 获取所有支持的语言名称,用逗号分隔(用于错误提示) + * + * @return 如 "java, go, python, javascript" + */ + public static String supportedLanguages() { + StringBuilder sb = new StringBuilder(); // 用 StringBuilder 拼接字符串 + for (LanguageConfig config : values()) { // 遍历所有枚举值 + if (sb.length() > 0) sb.append(", "); // 非第一个元素前加逗号分隔 + sb.append(config.language); // 追加语言名称 + } + return sb.toString(); // 返回拼接结果 + } +} diff --git a/src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java b/src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java new file mode 100644 index 0000000..083db1f --- /dev/null +++ b/src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java @@ -0,0 +1,128 @@ +package cn.meowrain.aioj; // 包声明 + +import cn.hutool.core.io.FileUtil; // 文件操作工具(写入 input.txt) +import cn.hutool.core.io.resource.ResourceUtil; // 资源文件读取工具(测试用) +import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; // 执行代码请求 DTO +import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; // 执行代码响应 DTO +import cn.meowrain.aioj.utils.ProcessUtil; // 进程执行工具类 + +import java.io.File; // 文件路径操作 +import java.nio.charset.StandardCharsets; // UTF-8 字符集 +import java.util.*; // 集合工具类 + +/** + * 本地代码沙箱实现(继承模板方法基类) + * 直接在宿主机上编译和运行用户代码,不使用 Docker 隔离 + * 适用于开发测试环境,生产环境建议使用 DockerCodeSandbox + */ +public class NativeCodeSandbox extends AbstractCodeSandbox { + + /** + * 测试入口方法 + */ + public static void main(String[] args) { + NativeCodeSandbox sandbox = new NativeCodeSandbox(); // 创建本地沙箱实例 + + // 从资源文件中读取测试代码 + String code = ResourceUtil.readStr("testCode.multiargs/Main.java", StandardCharsets.UTF_8); + // 构建执行请求 + ExecuteCodeRequestDTO request = ExecuteCodeRequestDTO.builder() + .code(code) // 设置用户代码 + .language("java") // 设置编程语言 + .inputList(Arrays.asList("1 2", "3 4")) // 设置测试用例输入列表 + .build(); // 构建请求对象 + + ExecuteCodeResponseDTO response = sandbox.executeCode(request); // 执行代码 + System.out.println("状态: " + response.getStatus()); // 打印执行状态 + System.out.println("消息: " + response.getMessage()); // 打印执行消息 + System.out.println("输出: " + response.getOutputList()); // 打印输出列表 + if (response.getJudgeInfo() != null) { // 如果有判题信息 + System.out.println("耗时: " + response.getJudgeInfo().getTime() + "ms"); // 打印耗时 + System.out.println("内存: " + response.getJudgeInfo().getMemory() + "KB"); // 打印内存 + } + System.exit(0); // 强制退出 JVM + } + + // ==================== 实现模板方法的钩子 ==================== + + /** + * 本地执行无需初始化额外环境 + */ + @Override + protected void initEnvironment(String codePath, LanguageConfig config, long memoryLimitKB) { + // 本地执行不需要初始化环境(无 Docker 容器) + } + + /** + * 在宿主机上编译代码 + * 使用 ProcessUtil 执行编译命令 + * + * @return 编译成功返回 null,失败返回错误信息 + */ + @Override + protected String compile(String codePath, LanguageConfig config) throws Exception { + // 获取编译命令,将容器内路径 /app 替换为实际宿主机路径 + String[] compileCmd = config.getCompileCmd(); // 获取原始编译命令 + String[] localCmd = new String[compileCmd.length]; // 创建本地命令数组 + for (int i = 0; i < compileCmd.length; i++) { // 遍历每个命令部分 + localCmd[i] = compileCmd[i].replace("/app", codePath); // 将 /app 替换为本地路径 + } + + // 使用 ProcessUtil.run 执行编译命令 + ProcessUtil.ExecuteResult result = ProcessUtil.run( // 执行编译 + Arrays.asList(localCmd), // 命令列表 + null, // 无 stdin 输入 + COMPILE_TIMEOUT_MS); // 编译超时时间 + + if (result.isTimeout()) { // 编译超时 + return "编译超时"; // 返回超时错误 + } + if (result.getExitCode() != 0) { // 编译失败 + String error = result.getStderr().isEmpty() // 优先取 stderr + ? result.getStdout() : result.getStderr(); // stderr 为空则取 stdout + return "编译失败:\n" + error; // 返回编译错误 + } + return null; // 编译成功 + } + + /** + * 在宿主机上运行单个测试用例 + * 通过 stdin 传入输入,收集 stdout 输出 + */ + @Override + protected RunResult runCase(String input, String codePath, LanguageConfig config, + long timeLimitMs, long memoryLimitKB) throws Exception { + // 计算 JVM 堆内存限制(内存限制的 80%,最低 16MB) + long xmxMB = Math.max(16, (memoryLimitKB / 1024) * 80 / 100); + // 获取运行命令(替换内存占位符),并将 /app 替换为本地路径 + String[] runCmd = config.getRunCmd(xmxMB); // 获取运行命令模板 + List localCmd = new ArrayList<>(); // 构建本地命令列表 + for (String part : runCmd) { // 遍历命令每部分 + localCmd.add(part.replace("/app", codePath)); // 将 /app 替换为本地路径 + } + + // 使用 ProcessUtil.run 执行,通过 stdin 传入输入 + ProcessUtil.ExecuteResult result = ProcessUtil.run( // 执行运行命令 + localCmd, // 命令列表 + input, // 通过 stdin 传入测试输入 + timeLimitMs); // 运行超时时间 + + // 将 ProcessUtil 的结果转换为通用 RunResult + return new RunResult( + result.isTimeout(), // 是否超时 + false, // 本地执行无 OOM 检测(无 Docker cgroup) + result.getExitCode(), // 退出码 + result.getStdout(), // 标准输出 + result.getStderr(), // 标准错误 + result.getTime(), // 耗时 + result.getMemory()); // 内存使用 + } + + /** + * 本地执行无需额外清理(临时文件由基类统一删除) + */ + @Override + protected void cleanupEnvironment() { + // 本地执行不需要清理额外环境(无 Docker 容器) + } +}