提交代码
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(del \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\main\\\\java\\\\cn\\\\meowrain\\\\aioj\\\\JavaDockerCodeSandBox.java\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
217
src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java
Normal file
217
src/main/java/cn/meowrain/aioj/AbstractCodeSandbox.java
Normal file
@@ -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<String> 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<String> 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; // 返回执行响应
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java
Normal file
35
src/main/java/cn/meowrain/aioj/CodeSandboxFactory.java
Normal file
@@ -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"); // 列出支持的类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java
Normal file
366
src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java
Normal file
@@ -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<String> 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<Image> 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<Frame> callback = new ResultCallback.Adapter<Frame>() {
|
||||||
|
@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<Statistics> cb = new ResultCallback.Adapter<Statistics>() {
|
||||||
|
@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]; // 返回内存使用量
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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<String> outputList = new ArrayList<>();
|
|
||||||
long maxTime = 0;
|
|
||||||
long maxMemory = 0;
|
|
||||||
|
|
||||||
for (String input : inputList.isEmpty() ? Collections.singletonList("") : inputList) {
|
|
||||||
// 构建命令:java -cp <classpath> Main <args...>
|
|
||||||
// inputList 中每个元素按空格拆分为多个命令行参数
|
|
||||||
List<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> 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<String> outputList = new ArrayList<>();
|
|
||||||
long maxTime = 0;
|
|
||||||
long maxMemory = 0;
|
|
||||||
|
|
||||||
for (String input : inputList.isEmpty() ? Collections.singletonList("") : inputList) {
|
|
||||||
// 构建命令:java -cp <classpath> Main <args...>
|
|
||||||
// inputList 中每个元素按空格拆分为多个命令行参数
|
|
||||||
List<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
107
src/main/java/cn/meowrain/aioj/LanguageConfig.java
Normal file
107
src/main/java/cn/meowrain/aioj/LanguageConfig.java
Normal file
@@ -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(); // 返回拼接结果
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java
Normal file
128
src/main/java/cn/meowrain/aioj/NativeCodeSandbox.java
Normal file
@@ -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<String> 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 容器)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user