From e4a33b630e7660b14348475ee580f4454373e4ab Mon Sep 17 00:00:00 2001 From: meowrain Date: Thu, 12 Feb 2026 20:54:28 +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 | 3 +- .idea/CoolRequestCommonStatePersistent.xml | 6 + .idea/deployment.xml | 15 ++ .idea/webServers.xml | 14 ++ pom.xml | 7 + .../cn/meowrain/aioj/DockerCodeSandbox.java | 31 ++- .../resources/testCode.multiargs/Main.java | 9 +- .../meowrain/aioj/DockerCodeSandboxTest.java | 208 ++++++++++++++++++ 8 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 .idea/CoolRequestCommonStatePersistent.xml create mode 100644 .idea/deployment.xml create mode 100644 .idea/webServers.xml create mode 100644 src/test/java/cn/meowrain/aioj/DockerCodeSandboxTest.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6cc8f09..a1f7d11 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(del \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\main\\\\java\\\\cn\\\\meowrain\\\\aioj\\\\JavaDockerCodeSandBox.java\")" + "Bash(del \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\main\\\\java\\\\cn\\\\meowrain\\\\aioj\\\\JavaDockerCodeSandBox.java\")", + "Bash(if not exist \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\test\\\\java\\\\cn\\\\meowrain\\\\aioj\" mkdir \"C:\\\\Users\\\\meowr\\\\IdeaProjects\\\\codesandboxtest2\\\\src\\\\test\\\\java\\\\cn\\\\meowrain\\\\aioj\")" ] } } diff --git a/.idea/CoolRequestCommonStatePersistent.xml b/.idea/CoolRequestCommonStatePersistent.xml new file mode 100644 index 0000000..7fb60ef --- /dev/null +++ b/.idea/CoolRequestCommonStatePersistent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..710508a --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/webServers.xml b/.idea/webServers.xml new file mode 100644 index 0000000..197ee9e --- /dev/null +++ b/.idea/webServers.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index e5b69a1..190f5ad 100644 --- a/pom.xml +++ b/pom.xml @@ -42,5 +42,12 @@ compile + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + \ No newline at end of file diff --git a/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java b/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java index 9e2aa2f..6edc62b 100644 --- a/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java +++ b/src/main/java/cn/meowrain/aioj/DockerCodeSandbox.java @@ -14,6 +14,7 @@ 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.exception.NotFoundException; // 镜像/容器不存在异常 import com.github.dockerjava.api.model.*; // Docker 模型类 import com.github.dockerjava.core.DefaultDockerClientConfig; // Docker 客户端默认配置 import com.github.dockerjava.core.DockerClientConfig; // Docker 客户端配置接口 @@ -108,10 +109,17 @@ public class DockerCodeSandbox extends AbstractCodeSandbox { @Override protected RunResult runCase(String input, String codePath, LanguageConfig config, long timeLimitMs, long memoryLimitKB) throws Exception { - // 将测试输入写入 input.txt(容器内通过 cat 读取) + // 将测试输入写入 input.txt String inputFilePath = codePath + File.separator + "input.txt"; // input.txt 宿主机路径 FileUtil.writeString(input != null ? input : "", inputFilePath, StandardCharsets.UTF_8); // 写入输入内容 + // 使用 docker cp 将 input.txt 复制到容器内 /app + DockerClient client = getDockerClient(); + client.copyArchiveToContainerCmd(containerId) + .withHostResource(inputFilePath) // 宿主机文件路径 + .withRemotePath(CONTAINER_WORK_DIR) // 容器内目标路径 + .exec(); + // 计算 JVM 堆内存限制(容器内存的 80%,最低 16MB) long xmxMB = Math.max(16, (memoryLimitKB / 1024) * 80 / 100); String[] runCmd = config.getRunCmd(xmxMB); // 获取运行命令(替换内存占位符) @@ -168,10 +176,14 @@ public class DockerCodeSandbox extends AbstractCodeSandbox { synchronized (DockerCodeSandbox.class) { // 加锁防止并发拉取 if (readyImages.contains(image)) return; // 双重检查 DockerClient client = getDockerClient(); // 获取客户端 - List images = client.listImagesCmd() // 查询本地镜像 - .withImageNameFilter(image) // 按名称过滤 - .exec(); // 执行查询 - if (images.isEmpty()) { // 本地不存在 + boolean exists; + try { + client.inspectImageCmd(image).exec(); // 精确查找镜像 + exists = true; + } catch (NotFoundException e) { // 镜像不存在 + exists = false; + } + if (!exists) { // 本地不存在 System.out.println("拉取镜像: " + image); // 打印提示 try { client.pullImageCmd(image) // 拉取镜像 @@ -198,7 +210,6 @@ public class DockerCodeSandbox extends AbstractCodeSandbox { // 配置宿主机资源限制和安全选项 HostConfig hostConfig = HostConfig.newHostConfig() - .withBinds(new Bind(hostCodePath, new Volume(CONTAINER_WORK_DIR))) // 挂载代码目录到 /app .withMemory(memoryBytes) // 内存硬限制 .withMemorySwap(memoryBytes) // 禁用 swap .withCpuCount(1L) // 限制 1 个 CPU @@ -219,6 +230,14 @@ public class DockerCodeSandbox extends AbstractCodeSandbox { String id = container.getId(); // 获取容器 ID client.startContainerCmd(id).exec(); // 启动容器 + + // 使用 docker cp 将代码文件复制到容器 /app(替代 bind mount,兼容远程 Docker / WSL2) + client.copyArchiveToContainerCmd(id) + .withHostResource(hostCodePath) // 宿主机代码目录 + .withRemotePath(CONTAINER_WORK_DIR) // 容器内目标路径 + .withDirChildrenOnly(true) // 只复制目录内容,不含目录本身 + .exec(); + return id; // 返回容器 ID } diff --git a/src/main/resources/testCode.multiargs/Main.java b/src/main/resources/testCode.multiargs/Main.java index 7e2c50f..008d7be 100644 --- a/src/main/resources/testCode.multiargs/Main.java +++ b/src/main/resources/testCode.multiargs/Main.java @@ -3,13 +3,6 @@ import java.util.List; public class Main { public static void main(String[] args) { - List holder = new ArrayList<>(); - int mb = 1024 * 1024; - long i = 0; - while (true) { - holder.add(new byte[10 * mb]); - i++; - System.out.println("allocated ~" + (i * 10) + "MB"); - } + System.out.println("helloworld "); } } \ No newline at end of file diff --git a/src/test/java/cn/meowrain/aioj/DockerCodeSandboxTest.java b/src/test/java/cn/meowrain/aioj/DockerCodeSandboxTest.java new file mode 100644 index 0000000..cc67132 --- /dev/null +++ b/src/test/java/cn/meowrain/aioj/DockerCodeSandboxTest.java @@ -0,0 +1,208 @@ +package cn.meowrain.aioj; + +import cn.meowrain.aioj.dto.req.ExecuteCodeRequestDTO; +import cn.meowrain.aioj.dto.resp.ExecuteCodeResponseDTO; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DockerCodeSandboxTest { + + private DockerCodeSandbox sandbox; + + @BeforeEach + void setUp() { + sandbox = new DockerCodeSandbox(); + } + + // ==================== Java ==================== + + @Test + void testJava_HelloWorld() { + String code = """ + public class Main { + public static void main(String[] args) { + System.out.println("hello java"); + } + } + """; + ExecuteCodeResponseDTO resp = execute("java", code, List.of("")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals("hello java", resp.getOutputList().get(0)); + } + + @Test + void testJava_ReadStdin() { + String code = """ + import java.util.Scanner; + public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int a = sc.nextInt(), b = sc.nextInt(); + System.out.println(a + b); + } + } + """; + ExecuteCodeResponseDTO resp = execute("java", code, Arrays.asList("1 2", "10 20")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals(List.of("3", "30"), resp.getOutputList()); + } + + @Test + void testJava_CompileError() { + String code = """ + public class Main { + public static void main(String[] args) { + System.out.println("missing quote) + } + } + """; + ExecuteCodeResponseDTO resp = execute("java", code, List.of("")); + assertEquals(2, resp.getStatus(), "should be compile error"); + } + + // ==================== Python ==================== + + @Test + void testPython_HelloWorld() { + String code = """ + print("hello python") + """; + ExecuteCodeResponseDTO resp = execute("python", code, List.of("")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals("hello python", resp.getOutputList().get(0)); + } + + @Test + void testPython_ReadStdin() { + String code = """ + a, b = map(int, input().split()) + print(a + b) + """; + ExecuteCodeResponseDTO resp = execute("python", code, Arrays.asList("3 4", "100 200")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals(List.of("7", "300"), resp.getOutputList()); + } + + @Test + void testPython_RuntimeError() { + String code = """ + print(1 / 0) + """; + ExecuteCodeResponseDTO resp = execute("python", code, List.of("")); + assertEquals(3, resp.getStatus(), "should be runtime error"); + } + + // ==================== Go ==================== + + @Test + void testGo_HelloWorld() { + String code = """ + package main + import "fmt" + func main() { + fmt.Println("hello go") + } + """; + ExecuteCodeResponseDTO resp = execute("go", code, List.of("")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals("hello go", resp.getOutputList().get(0)); + } + + @Test + void testGo_ReadStdin() { + String code = """ + package main + import "fmt" + func main() { + var a, b int + fmt.Scan(&a, &b) + fmt.Println(a + b) + } + """; + ExecuteCodeResponseDTO resp = execute("go", code, Arrays.asList("5 6", "50 60")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals(List.of("11", "110"), resp.getOutputList()); + } + + @Test + void testGo_CompileError() { + String code = """ + package main + func main() { + x := 1 + } + """; + ExecuteCodeResponseDTO resp = execute("go", code, List.of("")); + assertEquals(2, resp.getStatus(), "should be compile error (unused variable)"); + } + + // ==================== JavaScript ==================== + + @Test + void testJavaScript_HelloWorld() { + String code = """ + console.log("hello js"); + """; + ExecuteCodeResponseDTO resp = execute("javascript", code, List.of("")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals("hello js", resp.getOutputList().get(0)); + } + + @Test + void testJavaScript_ReadStdin() { + String code = """ + const readline = require('readline'); + const rl = readline.createInterface({ input: process.stdin }); + rl.on('line', (line) => { + const [a, b] = line.split(' ').map(Number); + console.log(a + b); + rl.close(); + }); + """; + ExecuteCodeResponseDTO resp = execute("javascript", code, Arrays.asList("7 8", "70 80")); + assertEquals(1, resp.getStatus(), resp.getMessage()); + assertEquals(List.of("15", "150"), resp.getOutputList()); + } + + @Test + void testJavaScript_RuntimeError() { + String code = """ + undefined.foo(); + """; + ExecuteCodeResponseDTO resp = execute("javascript", code, List.of("")); + assertEquals(3, resp.getStatus(), "should be runtime error"); + } + + // ==================== 边界场景 ==================== + + @Test + void testUnsupportedLanguage() { + ExecuteCodeResponseDTO resp = execute("rust", "fn main() {}", List.of("")); + assertEquals(3, resp.getStatus()); + assertTrue(resp.getMessage().contains("不支持的语言")); + } + + // ==================== 辅助方法 ==================== + + private ExecuteCodeResponseDTO execute(String language, String code, List inputList) { + ExecuteCodeRequestDTO request = ExecuteCodeRequestDTO.builder() + .language(language) + .code(code) + .inputList(inputList) + .build(); + ExecuteCodeResponseDTO resp = sandbox.executeCode(request); + System.out.printf("[%s] status=%d, message=%s, output=%s%n", + language, resp.getStatus(), resp.getMessage(), resp.getOutputList()); + if (resp.getJudgeInfo() != null) { + System.out.printf(" -> time=%dms, memory=%dKB%n", + resp.getJudgeInfo().getTime(), resp.getJudgeInfo().getMemory()); + } + return resp; + } +}