diff --git a/.env.example b/.env.example index 48176d7..bfd3c58 100644 --- a/.env.example +++ b/.env.example @@ -140,7 +140,7 @@ CODE_RUNNER_MAX_CPU_CORES=2.0 CODE_RUNNER_MAX_MEMORY_MB=1024 CODE_RUNNER_MAX_TIMEOUT_SECS=30 CODE_RUNNER_MAX_OUTPUT_BYTES=1048576 -CODE_RUNNER_LANGUAGES=python,node +CODE_RUNNER_LANGUAGES=python,node,go,rust DOCKER_SOCKET_PATH=/var/run/docker.sock # Rate limits (governor keyed limiters) diff --git a/AGENTS.md b/AGENTS.md index d62c428..755148a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -185,7 +185,7 @@ Readers can execute fenced code blocks in isolated Docker containers; authors ge **Governor 0.8 caveat**: `Quota::per_day` does not exist; `CODE_EXEC_DAILY_LIMITER` uses `Quota::with_period(24h).allow_burst(daily)`. `RATE_LIMIT_CODE_EXEC_PER_SEC` must be an integer (governor's `per_second` takes `NonZeroU32`; decimals in `.env` fall back to the default 1). -**Runner images** (`docker/`): `build-runners.sh` builds `yggdrasil-runner-base` → `yggdrasil-runner-python` → `yggdrasil-runner-node`; tags must match `LANGUAGES` image fields. Python image symlinks `python`→`python3` to match `run_cmd`. `runner.toml` files are image self-descriptions only — runtime config is the Rust `LANGUAGES` registry + `CODE_RUNNER_*` env, not parsed from toml. +**Runner images** (`docker/`): `build-runners.sh` builds `yggdrasil-runner-base` → `yggdrasil-runner-python` → `yggdrasil-runner-node` → `yggdrasil-runner-go` → `yggdrasil-runner-rust`; tags must match `LANGUAGES` image fields. Python image symlinks `python`→`python3` to match `run_cmd`. Go image redirects `GOCACHE`/`GOTMPDIR`/`GOPATH` to `/tmp` (read-only rootfs makes `$HOME/.cache` unwritable). Rust image ships a `/usr/local/bin/run-rust.sh` wrapper because `rustc` compile + run is two steps and `docker.rs` injects `run_cmd` via `exec` — `exec A && B` would replace the shell and never reach `B`, so the two steps are encapsulated in the wrapper. `runner.toml` files are image self-descriptions only — runtime config is the Rust `LANGUAGES` registry + `CODE_RUNNER_*` env, not parsed from toml. ## Frontend Lib Subprojects diff --git a/docker/build-runners.sh b/docker/build-runners.sh index bd2b599..4837e05 100755 --- a/docker/build-runners.sh +++ b/docker/build-runners.sh @@ -1,11 +1,13 @@ #!/bin/sh -# 构建代码运行器所需的沙箱镜像:base → python / node。 +# 构建代码运行器所需的沙箱镜像:base → python / node / go / rust。 # -# 依赖:本机已安装 docker。构建顺序固定(python/node FROM base)。 +# 依赖:本机已安装 docker。构建顺序固定(python/node/go/rust 均为 FROM base)。 # 镜像 tag 与 src/api/code_runner/languages.rs 的注册项严格对应: # yggdrasil-runner-base:latest # yggdrasil-runner-python:latest # yggdrasil-runner-node:latest +# yggdrasil-runner-go:latest +# yggdrasil-runner-rust:latest set -e SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) @@ -19,6 +21,12 @@ docker build -t yggdrasil-runner-python:latest "$SCRIPT_DIR/runner-python" echo "==> Building yggdrasil-runner-node:latest" docker build -t yggdrasil-runner-node:latest "$SCRIPT_DIR/runner-node" +echo "==> Building yggdrasil-runner-go:latest" +docker build -t yggdrasil-runner-go:latest "$SCRIPT_DIR/runner-go" + +echo "==> Building yggdrasil-runner-rust:latest" +docker build -t yggdrasil-runner-rust:latest "$SCRIPT_DIR/runner-rust" + echo "==> Done. Images:" docker images --filter "reference=yggdrasil-runner-*" \ --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" diff --git a/docker/runner-go/Dockerfile b/docker/runner-go/Dockerfile new file mode 100644 index 0000000..d86901f --- /dev/null +++ b/docker/runner-go/Dockerfile @@ -0,0 +1,21 @@ +# Go 运行沙箱镜像。 +# 与 src/api/code_runner/languages.rs 的 go 注册项对齐: +# image: yggdrasil-runner-go:latest +# run_cmd: go run /code/main.go +# +# go run 是单条命令(内部编译 + 运行),可直接作为 run_cmd(无需 wrapper)。 +# 注意:运行时只读根文件系统 + tmpfs /code /tmp,$HOME/.cache 不可写, +# 必须把 GOCACHE / GOTMPDIR / GOPATH 重定向到可写的 /tmp tmpfs。 +FROM yggdrasil-runner-base:latest + +RUN apk add --no-cache go \ + && mkdir -p /tmp/go-cache /tmp/gopath \ + && chown -R runner:runner /tmp/go-cache /tmp/gopath + +ENV GOCACHE=/tmp/go-cache \ + GOTMPDIR=/tmp \ + GOPATH=/tmp/gopath + +COPY runner.toml /runner.toml + +USER runner diff --git a/docker/runner-go/runner.toml b/docker/runner-go/runner.toml new file mode 100644 index 0000000..dae15b6 --- /dev/null +++ b/docker/runner-go/runner.toml @@ -0,0 +1,15 @@ +# Go runner 镜像的运行时默认值(参考用)。 +# 实际生效配置由 Rust 侧 LANGUAGES 注册表 + CODE_RUNNER_* 环境变量决定(见 +# src/api/code_runner/languages.rs、src/infra/runner_config.rs),本文件仅作为 +# 镜像构建产物的自描述,便于运维核对镜像与代码的契约一致。 +language = "go" +extension = "go" +run_command = "go run /code/main.go" +allow_network = false + +[default_limits] +cpu_cores = 1.0 +memory_mb = 256 +timeout_secs = 10 +output_bytes = 1048576 +allow_network = false diff --git a/docker/runner-rust/Dockerfile b/docker/runner-rust/Dockerfile new file mode 100644 index 0000000..e2bd8b7 --- /dev/null +++ b/docker/runner-rust/Dockerfile @@ -0,0 +1,18 @@ +# Rust 运行沙箱镜像。 +# 与 src/api/code_runner/languages.rs 的 rust 注册项对齐: +# image: yggdrasil-runner-rust:latest +# run_cmd: /usr/local/bin/run-rust.sh +# +# rustc 编译 + 运行是两步,但 docker.rs 的注入脚本用 exec 执行 run_cmd, +# exec 会替换 shell 进程,导致 "A && B" 的后半段不执行。 +# 因此镜像内置 run-rust.sh wrapper 封装「编译并运行」逻辑。 +FROM yggdrasil-runner-base:latest + +RUN apk add --no-cache rust + +COPY run-rust.sh /usr/local/bin/run-rust.sh +RUN chmod +x /usr/local/bin/run-rust.sh + +COPY runner.toml /runner.toml + +USER runner diff --git a/docker/runner-rust/run-rust.sh b/docker/runner-rust/run-rust.sh new file mode 100644 index 0000000..ca53b1e --- /dev/null +++ b/docker/runner-rust/run-rust.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Rust 源码编译并运行的 wrapper。 +# +# 背景:src/infra/docker.rs 的源码注入脚本为 +# sh -c "cat > /code/main.rs && exec {run_cmd}" +# 其中 exec 会用 run_cmd 进程替换 sh 进程。若直接写 +# "rustc -o /tmp/main /code/main.rs && /tmp/main" +# exec 替换后 && 后半段永远不会执行(exec 成功即不再返回 shell)。 +# 因此必须把「编译 + 运行」封装进本脚本,由 sh 解释执行: +# rustc 编译成功后,再用 exec 替换为编译产物,避免多一层常驻进程。 +# +# 编译产物写到 /tmp/main(tmpfs 64m 可写;只读根文件系统下 / 不可写)。 +rustc -o /tmp/main /code/main.rs && exec /tmp/main diff --git a/docker/runner-rust/runner.toml b/docker/runner-rust/runner.toml new file mode 100644 index 0000000..f416546 --- /dev/null +++ b/docker/runner-rust/runner.toml @@ -0,0 +1,19 @@ +# Rust runner 镜像的运行时默认值(参考用)。 +# 实际生效配置由 Rust 侧 LANGUAGES 注册表 + CODE_RUNNER_* 环境变量决定(见 +# src/api/code_runner/languages.rs、src/infra/runner_config.rs),本文件仅作为 +# 镜像构建产物的自描述,便于运维核对镜像与代码的契约一致。 +# +# run_command 指向镜像内置的 wrapper 脚本(run-rust.sh),而非裸 rustc 命令: +# rustc 编译 + 运行是两步,docker.rs 注入脚本用 exec 执行 run_cmd, +# exec 替换 shell 进程后 "A && B" 后半段不执行,故封装进脚本。 +language = "rust" +extension = "rs" +run_command = "/usr/local/bin/run-rust.sh" +allow_network = false + +[default_limits] +cpu_cores = 1.0 +memory_mb = 512 +timeout_secs = 15 +output_bytes = 1048576 +allow_network = false diff --git a/src/api/code_runner/languages.rs b/src/api/code_runner/languages.rs index df785d5..9cc3172 100644 --- a/src/api/code_runner/languages.rs +++ b/src/api/code_runner/languages.rs @@ -63,6 +63,46 @@ pub static LANGUAGES: LazyLock> = LazyLock::new(|| }, ); + // 编译型语言:go run 是单条命令(内部编译 + 运行),可直接作为 run_cmd。 + // 只读根文件系统下 $HOME/.cache 不可写,镜像已把 GOCACHE/GOTMPDIR/GOPATH + // 重定向到可写的 /tmp tmpfs。编译冷启动比解释型慢,timeout 提到 10s。 + m.insert( + "go".to_string(), + LanguageDef { + image: "yggdrasil-runner-go:latest".to_string(), + run_cmd: "go run /code/main.go".to_string(), + extension: "go".to_string(), + default_limits: ResourceLimits { + cpu_cores: 1.0, + memory_mb: 256, + timeout_secs: 10, + output_bytes: 1_048_576, + allow_network: false, + }, + allow_network: false, + }, + ); + + // rustc 编译 + 运行是两步,但 docker.rs 注入脚本用 exec 执行 run_cmd, + // exec 替换 shell 进程后 "A && B" 后半段不执行,故镜像内置 run-rust.sh wrapper。 + // rustc 内存开销大、编译慢,memory 提到 512MB、timeout 提到 15s。 + m.insert( + "rust".to_string(), + LanguageDef { + image: "yggdrasil-runner-rust:latest".to_string(), + run_cmd: "/usr/local/bin/run-rust.sh".to_string(), + extension: "rs".to_string(), + default_limits: ResourceLimits { + cpu_cores: 1.0, + memory_mb: 512, + timeout_secs: 15, + output_bytes: 1_048_576, + allow_network: false, + }, + allow_network: false, + }, + ); + m });