提交代码

This commit is contained in:
2026-02-12 19:25:51 +08:00
parent 50a71d88ba
commit b9849070bb
8 changed files with 860 additions and 264 deletions

View 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; // 默认内存限制 256MBKB
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; // 返回执行响应
}
}

View 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"); // 列出支持的类型
}
}
}

View 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]; // 返回内存使用量
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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、Gofalse 表示不需要(如 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(); // 返回拼接结果
}
}

View 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 容器)
}
}