上游 QingdaoU/JudgeServer 停更在 2024-04(registry 上的 latest 和 1.6.1 是同一份 镜像,编译器停在 gcc-13),没有新版可拉,所以自己重编。docker/judge/ 是只改工具链 的 Dockerfile 分叉,server/ 和 Judger/ 从上游固定 commit b28aa56 拉,一行没动。 镜像 oj2-judge-2(不在任何 registry 上:本机 build.sh --save → scp → docker load) - gcc/g++ 13 → 14.2,Python 3.12 → 3.13.5,都是 trixie 默认 - Go / JDK / Node 整套删掉:前端的题目语言复选框从来只给 C / C++ / Python / SQL, 12 万条提交里 Java 44 条、Golang 15、JavaScript 3,全是很早以前的 - 体积 1.1GB → 433MB;默认走清华源,构建 12 分钟 → 40 秒(--no-mirror 换回官方) - deploy.sh 加一道自检:镜像不在本机就中止,并打印该跑的三条命令 C 的编译参数加三个 -Wno-error(implicit-function-declaration / int-conversion / incompatible-pointer-types):gcc-14 把它们从 warning 提成了 error,而 -w 压不住。 语言值统一成 Python(迁移 0019 / 0020) - 0019:Python3(104527 条提交)与 Python2(3 条)并成 Python,一并改掉 937 道题的 languages、75 个 template 键、15 个 ast_rules 键、257 条 answers、1235 个用户的 成就指标 _languages(languages_used 重算,总和 1928 → 1925,少的 3 个是同时用过 两种 Python 的人) - 0020:把 Java / JavaScript / Golang 从 84 道题的可选语言里摘掉 —— 不摘的话那些题 的语言下拉还能选 Java,提交必 SYSTEM_ERROR - 契约新增 normalizeLanguage() 别名表,判题侧一律走 judgeConfigFor():旧客户端 localStorage 里的 Python3、迁移前排进队列的任务都还能判;协作的语言归一也走它, 否则上线那一刻学生页面里的 Python3 会静默落到 C - 回滚要连数据一起回,只滚代码会让所有 Python 提交变 SYSTEM_ERROR 实跑 - 判题冒烟 docker/judge/smoke.ts 13 条全过:三种语言、六种状态码、gcc 宽松度 - 拿备份里的真实代码逐文件比对新旧镜像的编译结果,0 差异:C 提交 1951 份 (1725 过 / 226 CE)、C++ 882 份、Python 2000 份、20 篇 C 教程的 93 个代码块。 不加那三个 -Wno-error 的话,C 有 26 份会从能过变成 CE - 迁移在灌了 12.4 万行真实数据的一次性库里跑过:0 残留、没有题目被清空; dev 库用真正的执行器跑通 - check:ast 56 个 target 全过,前后端 typecheck 均 0 顺带记下一个升级之前就有的坑(现在随 Go 一起消失,写在 README 里):GOCACHE 指向 容器的 tmpfs,判题机重启后第一次 Go 提交是冷构建,Go 1.22 要 5.6 秒 CPU、超过 3 秒 的编译预算,于是重启后第一个交 Go 的学生必吃一次 CE,后面的人缓存热了又都正常。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# 判题沙箱镜像。**这是 QingdaoU/JudgeServer 官方 Dockerfile 的分叉**,
|
||||
# 只改工具链版本,server/ 和 Judger/ 的代码一行都没动(构建时从上游仓库
|
||||
# 的固定 commit 拉,见 build.sh)。
|
||||
#
|
||||
# 为什么要分叉:上游停更在 2024-04-05(b28aa56,也就是 1.6.1 这个 tag),
|
||||
# registry 上的 `latest` 和 `1.6.1` 是同一份镜像,没有新版可升。想要新编译器
|
||||
# 只能自己构建。
|
||||
#
|
||||
# 相对上游的全部改动:
|
||||
# gcc/g++ 13 → 14 (trixie 默认)
|
||||
# Python 3.12 → 3.13 (trixie 默认)
|
||||
# Go / JDK / Node → **整套删掉**(见下)
|
||||
#
|
||||
# 只留 C / C++ / Python3 的工具链:前端的语言复选框从来只给这三种加 SQL,
|
||||
# 生产库 12 万条提交里 Java/Golang/JavaScript 一共 62 条、全是很早以前的。
|
||||
# 删掉 golang-1.24-go、temurin-25-jdk、nodejs 之后镜像从 1.16GB 掉到 ~500MB,
|
||||
# 构建也少了 NodeSource 那个 39MB 的 deb(它没有可用的国内镜像,最慢的一块)。
|
||||
# 想恢复某种语言:这里加回包 + alternatives,同时改 apps/api/src/judge/languages.ts。
|
||||
#
|
||||
# ⚠️ 语言的编译/运行命令**不在这个文件里**,在 apps/api/src/judge/languages.ts。
|
||||
# 升 gcc 大版本要同步看那边的开关(gcc-14 把 implicit-function-declaration
|
||||
# 等提成了 error,languages.ts 里有三个 -Wno-error 把它压回去)。
|
||||
|
||||
# 镜像源。默认走国内镜像 —— 官方源在这边实测 197 KB/s,清华 3.9 MB/s,
|
||||
# 整个构建从 12 分钟掉到 2 分钟出头。出国内网络环境用 build.sh --no-mirror 关掉。
|
||||
#
|
||||
# ⚠️ debian 这两个只能用 **http**:改 sources 这一步发生在装 ca-certificates 之前,
|
||||
# base 镜像里没有 CA 根证书,https 一律 `certificate verify failed`。
|
||||
# PyPI 那条是 pip 自己请求的,pip 内置 certifi,不依赖系统 CA。
|
||||
ARG APT_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
ARG APT_SECURITY_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
FROM debian:trixie-slim AS builder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG APT_MIRROR
|
||||
ARG APT_SECURITY_MIRROR
|
||||
ARG PIP_INDEX_URL
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,id=apt-cahce-1-$TARGETARCH$TARGETVARIANT-builder,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,id=apt-cahce-2-$TARGETARCH$TARGETVARIANT-builder,sharing=locked \
|
||||
<<EOS
|
||||
set -ex
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "1";' > /etc/apt/apt.conf.d/keep-cache
|
||||
echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/no-recommends
|
||||
echo 'APT::AutoRemove::RecommendsImportant "0";' >> /etc/apt/apt.conf.d/no-recommends
|
||||
# 换镜像源。debian-security 必须先换:它的 URI 以 debian 的 URI 为前缀,
|
||||
# 反过来的话第二条 sed 会把它改成 <镜像>/debian-security,路径不存在。
|
||||
sed -i "s|http://deb.debian.org/debian-security|$APT_SECURITY_MIRROR|; s|http://deb.debian.org/debian|$APT_MIRROR|" /etc/apt/sources.list.d/debian.sources
|
||||
apt-get update
|
||||
apt-get install -y libtool make cmake libseccomp-dev gcc python3 python3-venv
|
||||
EOS
|
||||
|
||||
COPY Judger/ /app/
|
||||
RUN <<EOS
|
||||
set -ex
|
||||
mkdir /app/build
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
EOS
|
||||
|
||||
RUN <<EOS
|
||||
set -ex
|
||||
cd bindings/Python
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip3 install -i "$PIP_INDEX_URL" build
|
||||
.venv/bin/python3 -m build -w
|
||||
EOS
|
||||
|
||||
FROM debian:trixie-slim
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
ARG APT_MIRROR
|
||||
ARG APT_SECURITY_MIRROR
|
||||
ARG PIP_INDEX_URL
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,id=apt-cahce-1-$TARGETARCH$TARGETVARIANT-final,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,id=apt-cahce-2-$TARGETARCH$TARGETVARIANT-final,sharing=locked \
|
||||
<<EOS
|
||||
set -ex
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "1";' > /etc/apt/apt.conf.d/keep-cache
|
||||
echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/no-recommends
|
||||
echo 'APT::AutoRemove::RecommendsImportant "0";' >> /etc/apt/apt.conf.d/no-recommends
|
||||
needed="python3.13-minimal \
|
||||
python3.13-venv \
|
||||
libpython3.13-stdlib \
|
||||
libpython3.13-dev \
|
||||
gcc-14 \
|
||||
g++-14 \
|
||||
strace"
|
||||
savedAptMark="$(apt-mark showmanual) $needed"
|
||||
# 换镜像源。debian-security 必须先换:它的 URI 以 debian 的 URI 为前缀,
|
||||
# 反过来的话第二条 sed 会把它改成 <镜像>/debian-security,路径不存在。
|
||||
sed -i "s|http://deb.debian.org/debian-security|$APT_SECURITY_MIRROR|; s|http://deb.debian.org/debian|$APT_MIRROR|" /etc/apt/sources.list.d/debian.sources
|
||||
apt-get update
|
||||
apt-get install -y $needed
|
||||
# languages.ts 里的编译/运行命令写的是绝对路径(/usr/bin/gcc、/usr/bin/python3 …),
|
||||
# 全靠下面这几条 alternatives 挂出来。换包名必须同步改,漏一条的表现是 CE 而不是报错。
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14
|
||||
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 14
|
||||
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.13 13
|
||||
apt-mark auto '.*' > /dev/null
|
||||
apt-mark manual $savedAptMark
|
||||
apt-get purge -y --auto-remove
|
||||
EOS
|
||||
|
||||
COPY --from=builder --chmod=755 --link /app/output/libjudger.so /usr/lib/judger/libjudger.so
|
||||
COPY --from=builder /app/bindings/Python/dist/ /app/
|
||||
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cahce-$TARGETARCH$TARGETVARIANT-final \
|
||||
<<EOS
|
||||
set -ex
|
||||
python3 -m venv .venv
|
||||
CC=gcc .venv/bin/pip3 install -i "$PIP_INDEX_URL" --compile --no-cache-dir flask gunicorn idna psutil requests
|
||||
.venv/bin/pip3 install *.whl
|
||||
EOS
|
||||
|
||||
COPY server/ /app/
|
||||
RUN <<EOS
|
||||
set -ex
|
||||
chmod -R u=rwX,go=rX /app/
|
||||
chmod +x /app/entrypoint.sh
|
||||
gcc -shared -fPIC -o unbuffer.so unbuffer.c
|
||||
useradd -u 901 -r -s /sbin/nologin -M compiler
|
||||
useradd -u 902 -r -s /sbin/nologin -M code
|
||||
useradd -u 903 -r -s /sbin/nologin -M -G code spj
|
||||
mkdir -p /usr/lib/judger
|
||||
EOS
|
||||
|
||||
# 构建期自检:这五行任何一个不是预期版本,说明上面的包名或 alternatives 配错了。
|
||||
RUN <<EOS
|
||||
set -ex
|
||||
gcc --version
|
||||
g++ --version
|
||||
python3 --version
|
||||
EOS
|
||||
|
||||
HEALTHCHECK --interval=5s CMD [ "/app/.venv/bin/python3", "/app/service.py" ]
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
@@ -0,0 +1,104 @@
|
||||
# 判题沙箱镜像
|
||||
|
||||
判题机跑的是 [QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer)。
|
||||
**上游已经停更**:master 最后一次提交是 2024-04-05(`b28aa56`,即 tag `v1.6.1`),
|
||||
registry 上的 `oj-image/judge:latest` 和 `:1.6.1` 是同一份镜像(config digest 都是
|
||||
`221bf4c0e730`)。所以想换新编译器,只能自己构建。
|
||||
|
||||
这里放的是**只改工具链的分叉**:`server/`(Flask + `_judger` 那套判题逻辑)和
|
||||
`Judger/`(libjudger.so 沙箱内核)一行都没动,构建时从上游那个固定 commit 拉。
|
||||
|
||||
## 工具链
|
||||
|
||||
2026-09 从上游镜像升级,同时把用不上的三种语言整套砍掉:
|
||||
|
||||
| | 上游 1.6.1 | 现在 |
|
||||
|---|---|---|
|
||||
| gcc / g++ | 13 | **14.2**(trixie 默认) |
|
||||
| Python | 3.12 | **3.13.5**(trixie 默认) |
|
||||
| Go | 1.22 | **删掉** |
|
||||
| Node | 20.x | **删掉** |
|
||||
| JDK | temurin-21 | **删掉** |
|
||||
| base | debian:trixie-slim(2024 年的) | debian:trixie-slim(当前) |
|
||||
| 镜像体积 | 1.1 GB | **433 MB** |
|
||||
|
||||
砍语言的依据:前端的题目语言复选框从来只给 Python / C / C++ / SQL,
|
||||
生产库 12 万条提交里 Java 44 条、Golang 15 条、JavaScript 3 条,全是很早以前的。
|
||||
契约 `judgeLanguageSchema` 里那几个键留着(渲染历史提交要用),只是判题机不再认。
|
||||
要恢复某种语言:Dockerfile 里加回包和 `update-alternatives`,同时改
|
||||
`apps/api/src/judge/languages.ts`,两边缺一个都是静默失败。
|
||||
|
||||
## 镜像源
|
||||
|
||||
默认走清华源(`Dockerfile` 顶部三个 ARG)。官方源在这边实测 **197 KB/s**,清华
|
||||
**3.9 MB/s**,整个构建从 12 分钟掉到 1 分钟以内。
|
||||
|
||||
`build.sh --no-mirror` 换回官方源。debian 那两个只能用 http —— 改 sources 发生在
|
||||
装 ca-certificates 之前,base 镜像里没有 CA 根证书,https 一律
|
||||
`certificate verify failed`(这个坑踩过)。
|
||||
|
||||
## 构建与分发
|
||||
|
||||
```bash
|
||||
docker/judge/build.sh --save # 本机构建 + 导出 dist/oj2-judge-2.tar
|
||||
scp dist/oj2-judge-2.tar root@服务器:/root/OJDeploy/
|
||||
ssh root@服务器 'docker load -i /root/OJDeploy/oj2-judge-2.tar'
|
||||
# 机房那台同样来一遍 —— 两个站点各有各的判题沙箱
|
||||
```
|
||||
|
||||
之后正常 `docker/deploy.sh` 即可。
|
||||
|
||||
⚠️ **这个名字不在任何 registry 上。** 服务器上忘了 `docker load`,compose 会去
|
||||
pull 然后报找不到镜像(好在是响亮地失败,不是静默降级)。
|
||||
|
||||
⚠️ **改工具链就把末尾的序号 +1**(下一版叫 `oj2-judge-3`:`build.sh` 里的 `IMAGE`、
|
||||
compose 里三处,一起改)。`docker compose up -d` 不带 `--pull`,名字没变会静默用机器上
|
||||
的旧镜像。官方镜像算第 1 版,所以我们自己重编的从 `-2` 起。
|
||||
|
||||
回滚:把三个 compose 的 image 改回
|
||||
`registry.cn-hongkong.aliyuncs.com/oj-image/judge:1.6.1`,重新 `up -d`。别在服务器上
|
||||
`docker image prune` 把那份旧镜像清掉。
|
||||
|
||||
## 和 `languages.ts` 的关系
|
||||
|
||||
**编译和运行命令不在镜像里**,在 `apps/api/src/judge/languages.ts`。镜像只负责把
|
||||
`/usr/bin/gcc`、`/usr/bin/python3`、`/usr/bin/go`、`/usr/bin/node`、`/usr/bin/java`
|
||||
这些绝对路径挂到正确的版本上(Dockerfile 末尾的 `update-alternatives`)。
|
||||
|
||||
gcc-14 把隐式函数声明、int↔指针互赋、不兼容指针类型从 warning 提成了 error,`-w`
|
||||
压不住。`languages.ts` 里的 `cLooseErrors` 三个 `-Wno-error=` 就是为此加的 ——
|
||||
实测 1951 份历史 C 提交和 20 篇 C 教程的 93 个代码块,加了之后与 gcc-13 逐个文件
|
||||
结果完全一致;不加的话有一批会从能过变成 CE。
|
||||
|
||||
## 这次升级是怎么验的
|
||||
|
||||
不写测试,全是实跑。除了 `smoke.ts` 的 13 条,还拿**生产库备份里的真实代码**逐个
|
||||
文件对比了新旧镜像的编译结果(脚本是一次性的,结论记在这里):
|
||||
|
||||
| 语料 | 份数 | 老镜像 (gcc-13 / py3.12) | 新镜像 (gcc-14 / py3.13) | 差异 |
|
||||
|---|---|---|---|---|
|
||||
| 历史 C 提交(共 19262,随机抽样) | 1951 | 1725 过 / 226 CE | 一模一样 | **0** |
|
||||
| 历史 C++ 提交(全量) | 882 | 603 过 / 279 CE | 一模一样 | **0** |
|
||||
| 历史 Python 提交(共 104527,随机抽样) | 2000 | 1833 过 / 167 CE | 一模一样 | **0** |
|
||||
| 20 篇 C 教程里含 `int main` 的代码块 | 93 | 93 全过 | 一模一样 | **0** |
|
||||
|
||||
**不加 `cLooseErrors` 那三个开关的话,1951 份 C 提交里有 26 份会从「能过」变成 CE**
|
||||
(按比例算全库约 260 条),全是忘了 `#include <string.h>` 之类的隐式函数声明。
|
||||
教程那 93 块本身写得规范,加不加都全过。
|
||||
|
||||
## 换镜像后怎么验
|
||||
|
||||
```bash
|
||||
bun docker/judge/smoke.ts # 六种语言 + 六种状态码 + gcc 宽松度
|
||||
```
|
||||
|
||||
它直接打判题机的 `/judge`,不需要起后端、不需要库里有题。用的 `languageConfigs`
|
||||
就是线上那份,所以配置和镜像对不上会当场暴露。
|
||||
|
||||
**Go 那条坑记一下**(升级时发现的,升级之前就有):`GOCACHE` 指向容器的 tmpfs
|
||||
`/tmp`,判题机重启后第一次 Go 提交是冷构建,Go 1.22 要 5.6 秒 CPU、超过 3 秒的编译
|
||||
预算 —— 重启后第一个交 Go 的学生必吃一次 CE,后面的人缓存热了又都正常。Go 现在整个
|
||||
删掉了,但**以后加回任何需要编译缓存的语言,记得把编译预算放宽**。
|
||||
|
||||
判题机装好之后,后台「判题机列表」应该能看到它上线(心跳走
|
||||
`POST /api/judge-server/heartbeat`,5 秒一次)。
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 构建判题沙箱镜像(本机构建,产物用 docker save 传到服务器和机房)。
|
||||
#
|
||||
# docker/judge/build.sh # 构建并打 tag
|
||||
# docker/judge/build.sh --save # 顺便导出 tar(给 scp 用)
|
||||
# docker/judge/build.sh --no-cache # 不吃构建缓存
|
||||
# docker/judge/build.sh --no-mirror # 不走国内镜像源(默认走)
|
||||
#
|
||||
# 上游 JudgeServer 停更在 2024-04-05,registry 上的 1.6.1 == latest,没有新版
|
||||
# 可拉。这个脚本从上游那个固定 commit 拉源码(server/ 和 Judger/ 一行不改),
|
||||
# 只把 Dockerfile 换成 docker/judge/Dockerfile —— 新工具链的全部改动都在那里。
|
||||
#
|
||||
# 上传和切换见 docker/judge/README.md。
|
||||
|
||||
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||
|
||||
# 上游 master HEAD(= tag v1.6.1+judgeserver.1.6.1)。Judger 子模块的版本由这个
|
||||
# commit 自己钉住(d19a6dc),不用在这里再写一遍。
|
||||
UPSTREAM_REPO=https://github.com/QingdaoU/JudgeServer.git
|
||||
UPSTREAM_COMMIT=b28aa56d60fed7358a29d9bdeb9d86fcc06e41a7
|
||||
|
||||
# 我们自己重编的第几版判题镜像(官方那个算第 1 版,这是第 2 版)。
|
||||
# **改工具链就把末尾的序号 +1**(下一版叫 oj2-judge-3),别在同一个名字上重建 ——
|
||||
# compose 的 `up -d` 不带 --pull,名字没变就会静默用机器上的旧镜像。
|
||||
IMAGE=oj2-judge-2
|
||||
|
||||
SAVE=0
|
||||
BUILD_ARGS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--save) SAVE=1 ;;
|
||||
--no-cache) BUILD_ARGS+=(--no-cache) ;;
|
||||
# 默认用国内镜像源(Dockerfile 顶部的四个 ARG)。在能直连的网络里用这个关掉,
|
||||
# 换回 deb.debian.org / pypi.org。
|
||||
--no-mirror)
|
||||
BUILD_ARGS+=(
|
||||
--build-arg APT_MIRROR=http://deb.debian.org/debian
|
||||
--build-arg APT_SECURITY_MIRROR=http://deb.debian.org/debian-security
|
||||
--build-arg PIP_INDEX_URL=https://pypi.org/simple
|
||||
) ;;
|
||||
*) echo "未知参数:$arg(可用:--save、--no-cache、--no-mirror)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null || die "没装 docker"
|
||||
[ -f docker/judge/Dockerfile ] || die "docker/judge/Dockerfile 不见了,当前目录:$PWD"
|
||||
|
||||
src=$(mktemp -d)
|
||||
trap 'rm -rf "$src"' EXIT
|
||||
|
||||
say "拉上游源码 $UPSTREAM_COMMIT"
|
||||
git -c advice.detachedHead=false clone --quiet "$UPSTREAM_REPO" "$src"
|
||||
git -C "$src" -c advice.detachedHead=false checkout --quiet "$UPSTREAM_COMMIT"
|
||||
git -C "$src" submodule update --quiet --init --recursive
|
||||
ok "server/ 和 Judger/ 就位"
|
||||
|
||||
say "构建 $IMAGE"
|
||||
# 上下文是上游源码,Dockerfile 用我们自己的那份。
|
||||
docker build "${BUILD_ARGS[@]}" -f docker/judge/Dockerfile -t "$IMAGE" "$src"
|
||||
ok "$IMAGE"
|
||||
|
||||
say "镜像里的工具链"
|
||||
docker run --rm --entrypoint sh "$IMAGE" -c '
|
||||
printf "gcc %s\n" "$(gcc -dumpfullversion)"
|
||||
printf "g++ %s\n" "$(g++ -dumpfullversion)"
|
||||
printf "python3 %s\n" "$(python3 -V | cut -d" " -f2)"
|
||||
'
|
||||
|
||||
if [ "$SAVE" = 1 ]; then
|
||||
out="dist/${IMAGE/:/-}.tar"
|
||||
mkdir -p dist
|
||||
say "导出 $out"
|
||||
docker save "$IMAGE" -o "$out"
|
||||
ok "$(du -h "$out" | cut -f1) → scp 到服务器后 docker load -i"
|
||||
fi
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 判题沙箱冒烟测试 —— **换镜像之后跑这个**(项目不写测试,验证一律实跑)。
|
||||
*
|
||||
* bun docker/judge/smoke.ts # 打 .env 里的 JUDGE_SERVER_URL
|
||||
* JUDGE_SERVER_URL=http://localhost:8082 bun docker/judge/smoke.ts
|
||||
*
|
||||
* 直接打判题机的 /judge,不经过 api / worker / 队列,所以不需要起后端,也不需要
|
||||
* 库里有题 —— 测试点用 `test_case` 内联传进去,判题机会当场写进临时目录。
|
||||
*
|
||||
* 它核的是三件事:
|
||||
* 1. 三种语言都能编译、运行、判对(languageConfigs 就是线上那份,不是另抄的)
|
||||
* 2. 六种结果状态的**整数值**没变(落库的值,见 packages/contract/src/judge-status.ts)
|
||||
* 3. gcc 的宽松度没变 —— 忘了 #include 的老代码照样能过(gcc-14 默认会把它判 CE)
|
||||
*/
|
||||
import { createHash } from "node:crypto"
|
||||
|
||||
import { JudgeStatus } from "@oj2/contract"
|
||||
|
||||
import { languageConfigs } from "../../apps/api/src/judge/languages"
|
||||
|
||||
const url = process.env.JUDGE_SERVER_URL ?? "http://localhost:8081"
|
||||
const rawToken = process.env.JUDGE_SERVER_TOKEN
|
||||
if (!rawToken) {
|
||||
console.error("JUDGE_SERVER_TOKEN 没设 —— 在 OJ2 根目录跑,bun 会自己读 .env")
|
||||
process.exit(2)
|
||||
}
|
||||
const token = createHash("sha256").update(rawToken).digest("hex")
|
||||
|
||||
interface Case {
|
||||
language: string
|
||||
name: string
|
||||
code: string
|
||||
expect: number
|
||||
/** 默认 3000ms / 128MB,跑得慢或要撑爆内存的用例自己改 */
|
||||
cpu?: number
|
||||
memory?: number
|
||||
}
|
||||
|
||||
const sumTestCase = [{ input: "1 2\n", output: "3\n" }]
|
||||
|
||||
const cases: Case[] = [
|
||||
// ---------------------------------------------------------------- C
|
||||
{
|
||||
language: "C",
|
||||
name: "C 正常通过",
|
||||
expect: JudgeStatus.ACCEPTED,
|
||||
code: `#include <stdio.h>
|
||||
int main(void) {
|
||||
int a, b;
|
||||
scanf("%d %d", &a, &b);
|
||||
printf("%d\\n", a + b);
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
// 这条是升 gcc 的主要风险点:gcc-14 起 implicit-function-declaration 是
|
||||
// error,languages.ts 里的三个 -Wno-error 就是为它加的。这条挂了说明那些
|
||||
// 开关没生效 —— 后果是一批历史题解和 20 篇 C 教程的示例突然全 CE。
|
||||
language: "C",
|
||||
name: "C 忘了 #include 仍能过(gcc 宽松度)",
|
||||
expect: JudgeStatus.ACCEPTED,
|
||||
code: `int main(void) {
|
||||
int a, b;
|
||||
scanf("%d %d", &a, &b);
|
||||
printf("%d\\n", a + b);
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
language: "C",
|
||||
name: "C 答案错误",
|
||||
expect: JudgeStatus.WRONG_ANSWER,
|
||||
code: `#include <stdio.h>
|
||||
int main(void) {
|
||||
int a, b;
|
||||
scanf("%d %d", &a, &b);
|
||||
printf("%d\\n", a + b + 1);
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
language: "C",
|
||||
name: "C 编译错误",
|
||||
expect: JudgeStatus.COMPILE_ERROR,
|
||||
code: `int main(void) { return }`,
|
||||
},
|
||||
{
|
||||
language: "C",
|
||||
name: "C 运行超时",
|
||||
expect: JudgeStatus.CPU_TIME_LIMIT_EXCEEDED,
|
||||
cpu: 1000,
|
||||
code: `int main(void) {
|
||||
volatile long x = 0;
|
||||
while (1) x++;
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
// RLIMIT_AS 是 max_memory 的两倍,所以 malloc 会先成功一阵子再失败,
|
||||
// 退出时 ru_maxrss 已经超过 max_memory → judger 判 MLE 而不是 RE。
|
||||
language: "C",
|
||||
name: "C 内存超限",
|
||||
expect: JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
memory: 64 * 1024 * 1024,
|
||||
code: `#include <stdlib.h>
|
||||
#include <string.h>
|
||||
int main(void) {
|
||||
for (;;) {
|
||||
char *p = malloc(8 * 1024 * 1024);
|
||||
if (!p) return 1;
|
||||
memset(p, 1, 8 * 1024 * 1024);
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
language: "C",
|
||||
name: "C 运行时错误",
|
||||
expect: JudgeStatus.RUNTIME_ERROR,
|
||||
code: `int main(void) {
|
||||
int *p = 0;
|
||||
*p = 1;
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
// ---------------------------------------------------------------- Python
|
||||
{
|
||||
language: "Python",
|
||||
name: "Python 正常通过",
|
||||
expect: JudgeStatus.ACCEPTED,
|
||||
code: `a, b = map(int, input().split())
|
||||
print(a + b)`,
|
||||
},
|
||||
{
|
||||
language: "Python",
|
||||
name: "Python 编译错误",
|
||||
expect: JudgeStatus.COMPILE_ERROR,
|
||||
code: `def (:`,
|
||||
},
|
||||
{
|
||||
language: "Python",
|
||||
name: "Python 运行超时",
|
||||
expect: JudgeStatus.CPU_TIME_LIMIT_EXCEEDED,
|
||||
cpu: 1000,
|
||||
code: `while True:
|
||||
pass`,
|
||||
},
|
||||
{
|
||||
language: "Python",
|
||||
name: "Python 运行时错误",
|
||||
expect: JudgeStatus.RUNTIME_ERROR,
|
||||
code: `print(1 / 0)`,
|
||||
},
|
||||
// ---------------------------------------------------------------- C++
|
||||
{
|
||||
language: "C++",
|
||||
name: "C++ 正常通过",
|
||||
expect: JudgeStatus.ACCEPTED,
|
||||
code: `#include <iostream>
|
||||
int main() {
|
||||
int a, b;
|
||||
std::cin >> a >> b;
|
||||
std::cout << a + b << std::endl;
|
||||
return 0;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
language: "C++",
|
||||
name: "C++ 编译错误",
|
||||
expect: JudgeStatus.COMPILE_ERROR,
|
||||
code: `int main() { return }`,
|
||||
},
|
||||
]
|
||||
|
||||
const names: Record<number, string> = {
|
||||
[JudgeStatus.COMPILE_ERROR]: "CE",
|
||||
[JudgeStatus.WRONG_ANSWER]: "WA",
|
||||
[JudgeStatus.ACCEPTED]: "AC",
|
||||
[JudgeStatus.CPU_TIME_LIMIT_EXCEEDED]: "TLE(cpu)",
|
||||
[JudgeStatus.REAL_TIME_LIMIT_EXCEEDED]: "TLE(real)",
|
||||
[JudgeStatus.MEMORY_LIMIT_EXCEEDED]: "MLE",
|
||||
[JudgeStatus.RUNTIME_ERROR]: "RE",
|
||||
[JudgeStatus.SYSTEM_ERROR]: "SE",
|
||||
}
|
||||
const label = (code: number) => `${names[code] ?? "?"}(${code})`
|
||||
|
||||
async function runCase(item: Case) {
|
||||
const response = await fetch(new URL("/judge", url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"X-Judge-Server-Token": token,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language_config: languageConfigs[item.language],
|
||||
src: item.code,
|
||||
max_cpu_time: item.cpu ?? 3000,
|
||||
max_memory: item.memory ?? 128 * 1024 * 1024,
|
||||
test_case: sumTestCase,
|
||||
output: false,
|
||||
io_mode: {
|
||||
io_mode: "Standard IO",
|
||||
input: "input.txt",
|
||||
output: "output.txt",
|
||||
},
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
|
||||
const body = (await response.json()) as {
|
||||
err: string | null
|
||||
data: unknown
|
||||
}
|
||||
// 编译失败走 err 通道,不会有逐测试点的结果
|
||||
if (body.err === "CompileError") return { result: JudgeStatus.COMPILE_ERROR }
|
||||
if (body.err) throw new Error(`${body.err}: ${JSON.stringify(body.data)}`)
|
||||
|
||||
const results = body.data as { result: number; cpu_time: number }[]
|
||||
// 多个测试点取最坏的那个,和 run.ts 的口径一致
|
||||
const failed = results.find((r) => r.result !== JudgeStatus.ACCEPTED)
|
||||
return failed ?? results[0]!
|
||||
}
|
||||
|
||||
/** 判题机刚重建时 gunicorn 还没起来,先等它 —— 否则整屏都是连接被关。 */
|
||||
async function waitReady() {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const response = await fetch(new URL("/ping", url), {
|
||||
method: "POST",
|
||||
headers: { "X-Judge-Server-Token": token },
|
||||
})
|
||||
if (response.ok) return
|
||||
} catch {
|
||||
// 还没起来,接着等
|
||||
}
|
||||
await Bun.sleep(500)
|
||||
}
|
||||
console.error(`连不上判题机 ${url}(等了 30 秒)`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
await waitReady()
|
||||
|
||||
let failures = 0
|
||||
console.log(`判题机 ${url}\n`)
|
||||
for (const item of cases) {
|
||||
try {
|
||||
const got = await runCase(item)
|
||||
const pass = got.result === item.expect
|
||||
if (!pass) failures++
|
||||
const time = "cpu_time" in got ? ` ${got.cpu_time}ms` : ""
|
||||
console.log(
|
||||
`${pass ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"} ${item.name.padEnd(32)}` +
|
||||
` 期望 ${label(item.expect).padEnd(10)} 实得 ${label(got.result)}${time}`,
|
||||
)
|
||||
} catch (error) {
|
||||
failures++
|
||||
console.log(`\x1b[31m✗\x1b[0m ${item.name.padEnd(32)} ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
failures === 0
|
||||
? `\n\x1b[32m全部 ${cases.length} 条通过\x1b[0m`
|
||||
: `\n\x1b[31m${failures} / ${cases.length} 条不对\x1b[0m`,
|
||||
)
|
||||
process.exit(failures === 0 ? 0 : 1)
|
||||
Reference in New Issue
Block a user