Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c6f2e7611 | ||
|
|
79ca8ac3bd | ||
|
|
fa0e1ab66c |
No files matched your search
@@ -147,7 +147,7 @@ QingdaoU/JudgeServer 停更在 2024-04(官方镜像的 `latest` 和 `1.6.1`
|
|||||||
**服务器和机房各有各的判题沙箱,两边都要装。**
|
**服务器和机房各有各的判题沙箱,两边都要装。**
|
||||||
- 改工具链就把末尾序号 +1(下一版 `oj2-judge-3`)。`up -d` 不带 `--pull`,名字没变会静默用旧镜像。
|
- 改工具链就把末尾序号 +1(下一版 `oj2-judge-3`)。`up -d` 不带 `--pull`,名字没变会静默用旧镜像。
|
||||||
- 编译/运行命令在 `apps/api/src/judge/languages.ts`,不在镜像里。gcc-14 把隐式函数
|
- 编译/运行命令在 `apps/api/src/judge/languages.ts`,不在镜像里。gcc-14 把隐式函数
|
||||||
声明等提成了 error(`-w` 压不住),那边的 `cLooseErrors` 三个 `-Wno-error=` 就是
|
声明等提成了 error(`-w` 压不住),那边的 `cLooseErrors` 几个 `-Wno-error=` 就是
|
||||||
为此加的 —— 删掉它们等于让一批历史题解和 C 教程示例集体 CE。
|
为此加的 —— 删掉它们等于让一批历史题解和 C 教程示例集体 CE。
|
||||||
- **判题沙箱只认 C / C++ / Python。** Java / JavaScript / Golang 连同镜像里的
|
- **判题沙箱只认 C / C++ / Python。** Java / JavaScript / Golang 连同镜像里的
|
||||||
JDK / Node / Go 在 2026-09 一起砍了(前端本来就没给入口,12 万条提交里它们共 62 条),
|
JDK / Node / Go 在 2026-09 一起砍了(前端本来就没给入口,12 万条提交里它们共 62 条),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"build": "bun build --compile --target=bun-linux-x64 src/main.ts --outfile ../../dist/oj2-api",
|
"build": "bun build --compile --target=bun-linux-x64 src/main.ts --outfile ../../dist/oj2-api",
|
||||||
"seed:dev": "bun src/scripts/seed-dev.ts",
|
"seed:dev": "bun src/scripts/seed-dev.ts",
|
||||||
"recount": "bun --env-file=../../.env src/main.ts recount",
|
"recount": "bun --env-file=../../.env src/main.ts recount",
|
||||||
|
"rediagnose": "bun --env-file=../../.env src/main.ts rediagnose",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"check:routes": "bun src/scripts/check-route-shadowing.ts",
|
"check:routes": "bun src/scripts/check-route-shadowing.ts",
|
||||||
"check:ast": "bun src/scripts/check-ast-targets.ts",
|
"check:ast": "bun src/scripts/check-ast-targets.ts",
|
||||||
|
|||||||
@@ -19,17 +19,19 @@ import { normalizeLanguage } from "@oj2/contract"
|
|||||||
const defaultEnv = ["LANG=en_US.UTF-8", "LANGUAGE=en_US:en", "LC_ALL=en_US.UTF-8"]
|
const defaultEnv = ["LANG=en_US.UTF-8", "LANGUAGE=en_US:en", "LC_ALL=en_US.UTF-8"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gcc-14 起这三类老写法从 warning 提成了 error,而 `-w` 只关警告、压不住 error:
|
* gcc-14 起这五类老写法从 warning 提成了 error,而 `-w` 只关警告、压不住 error:
|
||||||
* 隐式函数声明(忘了 `#include <stdio.h>` 就用 printf)、int 与指针互赋、
|
* 隐式函数声明(忘了 `#include <stdio.h>` 就用 printf)、int 与指针互赋、
|
||||||
* 不兼容的指针类型。判题机镜像 2026-09 从 gcc-13 升到 14(见 docker/judge/),
|
* 不兼容的指针类型、`int main` 里写不带值的 `return;`(全库 4 条,gcc-13 下能跑,
|
||||||
* 不加这三个开关的话,**一批历史题解和 20 篇 C 教程的示例会突然全部 CE**。
|
* 放行后退出码不定,多半判运行时错误,说明会让补 return 0)、省略 int 的
|
||||||
|
* `main()`(全库判过的 45 条,其中 8 条 AC)。判题机镜像 2026-09 从 gcc-13 升到 14(见 docker/judge/),
|
||||||
|
* 不加这几个开关的话,**一批历史题解和 20 篇 C 教程的示例会突然全部 CE**。
|
||||||
*
|
*
|
||||||
* 只给 C 加:C++ 那边这些本来就是 error,g++ 升版不改判定。
|
* 只给 C 加:C++ 那边这些本来就是 error,g++ 升版不改判定。
|
||||||
* 哪天决定「就是要学生写规范」,是删掉这三行,不是改镜像 —— 删之前先拿
|
* 哪天决定「就是要学生写规范」,是删掉这几个开关,不是改镜像 —— 删之前先拿
|
||||||
* docs/c-tutorials/verify-code.sh 全量过一遍教程。
|
* docs/c-tutorials/verify-code.sh 全量过一遍教程。
|
||||||
*/
|
*/
|
||||||
const cLooseErrors =
|
const cLooseErrors =
|
||||||
"-Wno-error=implicit-function-declaration -Wno-error=int-conversion -Wno-error=incompatible-pointer-types"
|
"-Wno-error=implicit-function-declaration -Wno-error=int-conversion -Wno-error=incompatible-pointer-types -Wno-error=return-mismatch -Wno-error=implicit-int"
|
||||||
|
|
||||||
export const languageConfigs: Record<string, Record<string, unknown>> = {
|
export const languageConfigs: Record<string, Record<string, unknown>> = {
|
||||||
C: {
|
C: {
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export async function failAbandonedSubmission(submissionId: string, error: unkno
|
|||||||
*
|
*
|
||||||
* 诊断失败(读不到测试点、判题机没回回溯)就不写这个字段,学生看到的和原来一样。
|
* 诊断失败(读不到测试点、判题机没回回溯)就不写这个字段,学生看到的和原来一样。
|
||||||
*/
|
*/
|
||||||
async function diagnoseRuntimeError(
|
export async function diagnoseRuntimeError(
|
||||||
row: {
|
row: {
|
||||||
submission: typeof schema.submission.$inferSelect
|
submission: typeof schema.submission.$inferSelect
|
||||||
problem: typeof schema.problem.$inferSelect
|
problem: typeof schema.problem.$inferSelect
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* oj2-api sql-child # SQL 判题子进程,由服务自己 spawn,不该手动调
|
* oj2-api sql-child # SQL 判题子进程,由服务自己 spawn,不该手动调
|
||||||
* oj2-api migrate # 执行待办的数据库迁移,部署时由 docker/deploy.sh 调
|
* oj2-api migrate # 执行待办的数据库迁移,部署时由 docker/deploy.sh 调
|
||||||
* oj2-api recount # 把题目/用户的计数列、成就的已解锁数重算回与明细一致,默认只读预演
|
* oj2-api recount # 把题目/用户的计数列、成就的已解锁数重算回与明细一致,默认只读预演
|
||||||
|
* oj2-api rediagnose # 运行时错误的诊断按现在的规则重做,默认只读预演
|
||||||
*
|
*
|
||||||
* 用动态 import 而非顶层 import:这几个模块都有导入即执行的副作用
|
* 用动态 import 而非顶层 import:这几个模块都有导入即执行的副作用
|
||||||
* (Bun.serve、连 Redis 开消费者),静态导入会让 sql-child 也把整个服务拉起来。
|
* (Bun.serve、连 Redis 开消费者),静态导入会让 sql-child 也把整个服务拉起来。
|
||||||
@@ -40,6 +41,11 @@ switch (command) {
|
|||||||
const { recount } = await import("./scripts/recount")
|
const { recount } = await import("./scripts/recount")
|
||||||
process.exit(await recount({ apply: process.argv.slice(3).includes("--apply") }))
|
process.exit(await recount({ apply: process.argv.slice(3).includes("--apply") }))
|
||||||
}
|
}
|
||||||
|
// 同上:诊断规则改了之后,把库里已有的诊断重做一遍(要连判题机和测试点,只能在线上跑)
|
||||||
|
case "rediagnose": {
|
||||||
|
const { rediagnose } = await import("./scripts/rediagnose")
|
||||||
|
process.exit(await rediagnose({ apply: process.argv.slice(3).includes("--apply") }))
|
||||||
|
}
|
||||||
case "sql-child": {
|
case "sql-child": {
|
||||||
const { runSqlChild } = await import("./judge/sql/child")
|
const { runSqlChild } = await import("./judge/sql/child")
|
||||||
await runSqlChild()
|
await runSqlChild()
|
||||||
@@ -61,7 +67,7 @@ switch (command) {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
console.error(
|
console.error(
|
||||||
`未知子命令:${command}\n可用:serve | worker | migrate | recount | healthcheck | sql-child`,
|
`未知子命令:${command}\n可用:serve | worker | migrate | recount | rediagnose | healthcheck | sql-child`,
|
||||||
)
|
)
|
||||||
process.exit(2)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { and, eq, inArray, isNull, sql } from "drizzle-orm"
|
||||||
|
|
||||||
|
import { db, schema } from "../db"
|
||||||
|
import { diagnoseRuntimeError, templateForLanguage, type JudgeCase } from "../judge/run"
|
||||||
|
import { JudgeStatus } from "../judge/status"
|
||||||
|
import { parseProblemTemplate } from "../judge/template"
|
||||||
|
import { asRecord } from "../routes/helpers"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把库里运行时错误的诊断(`statistic_info.runtime_error`)按现在的规则重做一遍。
|
||||||
|
*
|
||||||
|
* 诊断是判题时写进库的,规则改了旧提交不会跟着变。2026-09 那次把 TypeError 的归类从
|
||||||
|
* 12 种扩到 30 多种,库里已有的诊断没有新的 kind,学生翻旧提交看到的还是兜底话。
|
||||||
|
*
|
||||||
|
* - **Python**:只重做**已经有诊断**的(诊断功能上线之前的提交没存过,不去补 ——
|
||||||
|
* 那是九千多次重跑,而且学生早就不看了)。和判题时一样,重跑第一个失败测试点拿回溯,
|
||||||
|
* 所以**要在能读到测试点、连得上判题机的地方跑**,也就是线上的 oj-api 容器。
|
||||||
|
* - **C / C++**:诊断只是失败测试点的信号和退出码,`info` 里本来就存着,不用重跑,
|
||||||
|
* 全部补上(原来一条都没有)。
|
||||||
|
*
|
||||||
|
* 比赛提交不诊断,和判题时同一个口径。重跑失败(测试点没了、判题机没回回溯)的保留原样。
|
||||||
|
*
|
||||||
|
* 默认只读,打出变化;确认无误再加 --apply 落库:
|
||||||
|
*
|
||||||
|
* docker compose -f docker/compose.debian.yml run --rm oj-api oj2-api rediagnose
|
||||||
|
* docker compose -f docker/compose.debian.yml run --rm oj-api oj2-api rediagnose --apply
|
||||||
|
*
|
||||||
|
* 重跑走的是判题机,挑没人做题的时候跑。只写 runtime_error 这一个键,不碰别的。
|
||||||
|
*/
|
||||||
|
/** jsonb 读回来的键顺序和新拼的对象不一样,按排好序的键比。诊断是一层的平对象 */
|
||||||
|
function sameKeys(value: unknown) {
|
||||||
|
const record = asRecord(value)
|
||||||
|
return value ? JSON.stringify(record, Object.keys(record).sort()) : "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rediagnose({ apply }: { apply: boolean }) {
|
||||||
|
const rows = await db
|
||||||
|
.select({ submission: schema.submission, problem: schema.problem })
|
||||||
|
.from(schema.submission)
|
||||||
|
.innerJoin(schema.problem, eq(schema.problem.id, schema.submission.problemId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.submission.result, JudgeStatus.RUNTIME_ERROR),
|
||||||
|
isNull(schema.submission.contestId),
|
||||||
|
inArray(schema.submission.language, ["Python", "C", "C++"]),
|
||||||
|
sql`(${schema.submission.language} <> 'Python' or ${schema.submission.statisticInfo} ? 'runtime_error')`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
`${rows.length} 条运行时错误要重新诊断${apply ? "" : "(只读预演,加 --apply 才写)"}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const changes = new Map<string, number>()
|
||||||
|
let changed = 0
|
||||||
|
let failed = 0
|
||||||
|
for (const [index, row] of rows.entries()) {
|
||||||
|
const cases = asRecord(row.submission.info).data
|
||||||
|
const failure = (Array.isArray(cases) ? (cases as JudgeCase[]) : [])
|
||||||
|
.toSorted((left, right) => Number(left.test_case) - Number(right.test_case))
|
||||||
|
.find((item) => item.result !== JudgeStatus.ACCEPTED)
|
||||||
|
|
||||||
|
const rawTemplate = templateForLanguage(row.problem.template, row.submission.language)
|
||||||
|
const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null
|
||||||
|
const source = template
|
||||||
|
? `${template.prepend}\n${row.submission.code}\n${template.append}`
|
||||||
|
: row.submission.code
|
||||||
|
const prependLines = template ? template.prepend.split("\n").length : 0
|
||||||
|
|
||||||
|
const next = failure
|
||||||
|
? await diagnoseRuntimeError(row, source, prependLines, failure).catch(() => null)
|
||||||
|
: null
|
||||||
|
const previous = asRecord(row.submission.statisticInfo).runtime_error
|
||||||
|
if (!next) {
|
||||||
|
failed++
|
||||||
|
} else if (sameKeys(next) !== sameKeys(previous)) {
|
||||||
|
changed++
|
||||||
|
const label = (value: unknown) => {
|
||||||
|
const info = asRecord(value)
|
||||||
|
if (!value) return "(无)"
|
||||||
|
if (info.type) return `${String(info.type)}/${String(info.kind ?? "-")}`
|
||||||
|
return `信号 ${String(info.signal ?? 0)} 退出码 ${String(info.exit_code ?? 0)}`
|
||||||
|
}
|
||||||
|
const key = `${row.submission.language} ${label(previous)} → ${label(next)}`
|
||||||
|
changes.set(key, (changes.get(key) ?? 0) + 1)
|
||||||
|
if (apply) {
|
||||||
|
await db
|
||||||
|
.update(schema.submission)
|
||||||
|
.set({
|
||||||
|
statisticInfo: sql`${schema.submission.statisticInfo} || jsonb_build_object('runtime_error', ${JSON.stringify(next)}::jsonb)`,
|
||||||
|
})
|
||||||
|
.where(eq(schema.submission.id, row.submission.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((index + 1) % 50 === 0) console.log(` ${index + 1} / ${rows.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, count] of [...changes].sort((left, right) => right[1] - left[1])) {
|
||||||
|
console.log(`${String(count).padStart(5)} ${key}`)
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`变了 ${changed} 条${apply ? ",已写入" : ""};没变 ${rows.length - changed - failed} 条;诊断不出来、保留原样 ${failed} 条`,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
#
|
#
|
||||||
# ⚠️ 语言的编译/运行命令**不在这个文件里**,在 apps/api/src/judge/languages.ts。
|
# ⚠️ 语言的编译/运行命令**不在这个文件里**,在 apps/api/src/judge/languages.ts。
|
||||||
# 升 gcc 大版本要同步看那边的开关(gcc-14 把 implicit-function-declaration
|
# 升 gcc 大版本要同步看那边的开关(gcc-14 把 implicit-function-declaration
|
||||||
# 等提成了 error,languages.ts 里有三个 -Wno-error 把它压回去)。
|
# 等提成了 error,languages.ts 里有几个 -Wno-error 把它压回去)。
|
||||||
|
|
||||||
# 镜像源。默认走国内镜像 —— 官方源在这边实测 197 KB/s,清华 3.9 MB/s,
|
# 镜像源。默认走国内镜像 —— 官方源在这边实测 197 KB/s,清华 3.9 MB/s,
|
||||||
# 整个构建从 12 分钟掉到 2 分钟出头。出国内网络环境用 build.sh --no-mirror 关掉。
|
# 整个构建从 12 分钟掉到 2 分钟出头。出国内网络环境用 build.sh --no-mirror 关掉。
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ compose 里三处,一起改)。`docker compose up -d` 不带 `--pull`,名
|
|||||||
这些绝对路径挂到正确的版本上(Dockerfile 末尾的 `update-alternatives`)。
|
这些绝对路径挂到正确的版本上(Dockerfile 末尾的 `update-alternatives`)。
|
||||||
|
|
||||||
gcc-14 把隐式函数声明、int↔指针互赋、不兼容指针类型从 warning 提成了 error,`-w`
|
gcc-14 把隐式函数声明、int↔指针互赋、不兼容指针类型从 warning 提成了 error,`-w`
|
||||||
压不住。`languages.ts` 里的 `cLooseErrors` 三个 `-Wno-error=` 就是为此加的 ——
|
压不住。`languages.ts` 里的 `cLooseErrors` 里的 `-Wno-error=` 就是为此加的 ——
|
||||||
实测 1951 份历史 C 提交和 20 篇 C 教程的 93 个代码块,加了之后与 gcc-13 逐个文件
|
实测 1951 份历史 C 提交和 20 篇 C 教程的 93 个代码块,加了之后与 gcc-13 逐个文件
|
||||||
结果完全一致;不加的话有一批会从能过变成 CE。
|
结果完全一致;不加的话有一批会从能过变成 CE。
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -54,7 +54,7 @@ int main(void) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 这条是升 gcc 的主要风险点:gcc-14 起 implicit-function-declaration 是
|
// 这条是升 gcc 的主要风险点:gcc-14 起 implicit-function-declaration 是
|
||||||
// error,languages.ts 里的三个 -Wno-error 就是为它加的。这条挂了说明那些
|
// error,languages.ts 里的几个 -Wno-error 就是为它加的。这条挂了说明那些
|
||||||
// 开关没生效 —— 后果是一批历史题解和 20 篇 C 教程的示例突然全 CE。
|
// 开关没生效 —— 后果是一批历史题解和 20 篇 C 教程的示例突然全 CE。
|
||||||
language: "C",
|
language: "C",
|
||||||
name: "C 忘了 #include 仍能过(gcc 宽松度)",
|
name: "C 忘了 #include 仍能过(gcc 宽松度)",
|
||||||
@@ -64,6 +64,24 @@ int main(void) {
|
|||||||
scanf("%d %d", &a, &b);
|
scanf("%d %d", &a, &b);
|
||||||
printf("%d\\n", a + b);
|
printf("%d\\n", a + b);
|
||||||
return 0;
|
return 0;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// gcc-14 把 int 函数里不带值的 return;(-Wreturn-mismatch)、省略 int 的
|
||||||
|
// main()(-Wimplicit-int)也提成了 error
|
||||||
|
language: "C",
|
||||||
|
name: "C 不带值的 return;、省略 int 的 main() 仍能过(gcc 宽松度)",
|
||||||
|
expect: JudgeStatus.ACCEPTED,
|
||||||
|
code: `#include <stdio.h>
|
||||||
|
int check(int a) {
|
||||||
|
if (a < 0) return;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
main(void) {
|
||||||
|
int a, b;
|
||||||
|
scanf("%d %d", &a, &b);
|
||||||
|
printf("%d\\n", check(a) + b);
|
||||||
|
return 0;
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user