Files
OJ2/apps/api/src/judge/sql/child.ts
T
xuyueandClaude Opus 5 ed56a209ea
Deploy / deploy (push) Has been cancelled
chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:27:34 -06:00

109 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SQL 判题子进程的入口。
*
* 单独起进程的唯一理由是**能被杀掉**:SQLite 的查询跑在原生代码里,
* 实测 Worker.terminate() 抢占不了,只有 OS 的 SIGKILL 可靠。
* 父进程见 `./index.ts`。
*
* 协议:stdin 读一段 JSON 作业,stdout 写一段 JSON 结果;
* 阶段标记写 stderr,父进程据此收紧兜底时限、并判断超时该算谁的。
*
* 这个文件**不是**独立入口,而是由 `src/main.ts` 的 `sql-child` 子命令调用。
* 原因:`bun build --compile` 之后磁盘上没有 child.ts 可以让父进程去 spawn
* 只能让二进制自己按 argv 分发到这里。
*/
import { writeSync } from "node:fs"
import { buildDisplay, runCase, SqlCaseError } from "./engine"
import { JudgeStatus } from "../status"
export type SqlJob =
| {
kind: "judge"
initSql: string
refSql: string
studentSql: string
mode: "query" | "modify"
orderSensitive: boolean
timeLimitMs: number
memoryLimitMb: number
}
| {
kind: "display"
initSql: string
refSql: string
mode: "query" | "modify"
}
/**
* 写阶段标记。必须用 writeSync:父进程正是靠这个标记决定「多久之后 SIGKILL」
* 以及「超时算谁的」,走异步 stderr 的话标记可能还在缓冲里就被杀掉了。
*/
function markPhase(phase: string) {
const bytes = new TextEncoder().encode(`@phase:${phase}\n`)
let written = 0
while (written < bytes.length) {
written += writeSync(2, bytes, written, bytes.length - written)
}
}
/**
* 写完结果立刻硬退出,不走 Bun 的正常 teardown。
*
* 实测在 `ulimit -v` 之下,Bun 退出时的清理有概率撞上地址空间上限而 panic
* SIGILLexit 132)—— 结果其实已经写到 stdout 了,但进程异常终止会让父进程
* 读到空串,进而误判成超时。这个 panic 是不确定的,同样的输入时好时坏,
* 正是最难查的那种。这里跳过 teardown:子进程本来就是一次性的,没有要优雅关闭的资源。
*/
function finish(payload: unknown): never {
const bytes = new TextEncoder().encode(JSON.stringify(payload))
// 直接写 fd 1 并确认写完,再自杀。不用 process.exit():实测它仍会走一段清理,
// 在 ulimit 之下有概率 panicSIGILL,6 次里 2 次),而结果早已写出,
// 父进程却因进程异常终止读到空串、误判成超时 —— 时好时坏,最难查的那种。
let written = 0
while (written < bytes.length) {
written += writeSync(1, bytes, written, bytes.length - written)
}
process.kill(process.pid, "SIGKILL")
throw new Error("unreachable")
}
/** 子进程入口。由 `src/main.ts` 的 `sql-child` 子命令调用,不在导入时自动执行 */
export async function runSqlChild() {
const raw = await new Response(Bun.stdin.stream()).text()
const job = JSON.parse(raw) as SqlJob
try {
if (job.kind === "display") {
markPhase("display")
const display = await buildDisplay(job.initSql, job.refSql, job.mode)
finish({ ok: true, display })
}
// 阶段由 runCase 内部回调标记:prepare(受信脚本)→ student(学生 SQL
const result = await runCase(job.initSql, job.refSql, job.studentSql, {
mode: job.mode,
orderSensitive: job.orderSensitive,
timeLimitMs: job.timeLimitMs,
memoryLimitMb: job.memoryLimitMb,
onPhase: markPhase,
})
finish({ ok: true, case: result })
} catch (error) {
if (error instanceof SqlCaseError) {
finish({ ok: false, result: error.result, message: error.detail })
}
// WASM 堆触顶时 emscripten 抛的是普通 Error"Aborted"/"out of memory"),
// 到这里说明连引擎自身都没撑住,按内存超限报,不当成出题人的错
const message = String((error as Error)?.message ?? error)
const memoryish =
message.includes("out of memory") || message.includes("Aborted")
finish({
ok: false,
result: memoryish
? JudgeStatus.MEMORY_LIMIT_EXCEEDED
: JudgeStatus.SYSTEM_ERROR,
message: memoryish ? "内存超出限制" : message.slice(0, 200),
})
}
}