1 parent
23844ae301
commit
d25b5dbe6d
268 files changed
+3684
-8861
No files matched your search
+11
-36
@@ -39,19 +39,13 @@ async function loadLanguage(language: string) {
|
||||
if (!AST_SUPPORTED_LANGUAGES.includes(language)) return null
|
||||
// locateFile 指到内嵌的 tree-sitter.wasm:emscripten 默认按脚本所在目录找,
|
||||
// 单二进制里那个目录是 /$bunfs/root,它自己找不着
|
||||
if (!initPromise)
|
||||
initPromise = Parser.init({ locateFile: () => treeSitterWasmPath })
|
||||
if (!initPromise) initPromise = Parser.init({ locateFile: () => treeSitterWasmPath })
|
||||
await initPromise
|
||||
|
||||
const cached = languages.get(language)
|
||||
if (cached) return cached
|
||||
|
||||
const wasmPath =
|
||||
language === "C"
|
||||
? cWasmPath
|
||||
: language === "C++"
|
||||
? cppWasmPath
|
||||
: pythonWasmPath
|
||||
const wasmPath = language === "C" ? cWasmPath : language === "C++" ? cppWasmPath : pythonWasmPath
|
||||
const loaded = await Language.load(wasmPath)
|
||||
languages.set(language, loaded)
|
||||
return loaded
|
||||
@@ -75,8 +69,7 @@ function targetName(rule: AstRule, language?: string) {
|
||||
|
||||
function countPhrase(verb: string, rule: AstRule) {
|
||||
if (rule.exact !== undefined) return `${verb} ${rule.exact} 次`
|
||||
if (rule.min !== undefined && rule.max !== undefined)
|
||||
return `${verb} ${rule.min}~${rule.max} 次`
|
||||
if (rule.min !== undefined && rule.max !== undefined) return `${verb} ${rule.min}~${rule.max} 次`
|
||||
if (rule.min !== undefined) return `至少${verb} ${rule.min} 次`
|
||||
if (rule.max !== undefined) return `至多${verb} ${rule.max} 次`
|
||||
return ""
|
||||
@@ -115,9 +108,7 @@ export function describeAstRule(rule: AstRule, language?: string): string {
|
||||
// 「必须使用 for_loop 嵌套」,旧栈 ast_checker/engines/nesting.py 是翻的
|
||||
const outer = astNodeLabel(rule.outer ?? "", language)
|
||||
const inner = astNodeLabel(rule.inner ?? "", language)
|
||||
return outer === inner
|
||||
? `必须使用 ${outer} 嵌套`
|
||||
: `必须在 ${outer} 中嵌套使用 ${inner}`
|
||||
return outer === inner ? `必须使用 ${outer} 嵌套` : `必须在 ${outer} 中嵌套使用 ${inner}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,15 +176,12 @@ export function astRulesError(astRules: AstRules | null): string | null {
|
||||
const at = `代码规则 ${language} 第 ${index + 1} 条`
|
||||
const target = rule.target ?? ""
|
||||
if (rule.engine.endsWith("_node")) {
|
||||
if (!(target in nodes))
|
||||
return `${at}:${language} 没有「${target}」这种语法`
|
||||
if (!(target in nodes)) return `${at}:${language} 没有「${target}」这种语法`
|
||||
} else if (rule.engine === "must_use_operator") {
|
||||
if (!(target in operators))
|
||||
return `${at}:${language} 没有「${target}」运算符`
|
||||
if (!(target in operators)) return `${at}:${language} 没有「${target}」运算符`
|
||||
} else if (rule.engine === "must_have_nesting") {
|
||||
for (const value of [rule.outer ?? "", rule.inner ?? ""]) {
|
||||
if (!(value in nodes))
|
||||
return `${at}:${language} 没有「${value}」这种语法`
|
||||
if (!(value in nodes)) return `${at}:${language} 没有「${value}」这种语法`
|
||||
}
|
||||
} else if (!target.trim()) {
|
||||
return `${at}:要检查的函数名/方法名不能为空`
|
||||
@@ -212,10 +200,7 @@ export function astRulesError(astRules: AstRules | null): string | null {
|
||||
* 早年配过 C++ 规则,如今 tab 里看不到那组规则,保存却被「暂不支持 C++」拦下,
|
||||
* 老师在界面上无从修改。
|
||||
*/
|
||||
export function pickAstRules(
|
||||
astRules: AstRules | null,
|
||||
languages: string[],
|
||||
): AstRules | null {
|
||||
export function pickAstRules(astRules: AstRules | null, languages: string[]): AstRules | null {
|
||||
if (!astRules) return null
|
||||
const out: AstRules = {}
|
||||
for (const [language, rules] of Object.entries(astRules)) {
|
||||
@@ -261,27 +246,17 @@ function methodCalls(root: Node, target: string, language: string) {
|
||||
if (language === "C++") {
|
||||
return collectNodes(root, "call_expression").filter((call) => {
|
||||
const fn = call.childForFieldName("function")
|
||||
return (
|
||||
fn?.type === "field_expression" &&
|
||||
fn.childForFieldName("field")?.text === target
|
||||
)
|
||||
return fn?.type === "field_expression" && fn.childForFieldName("field")?.text === target
|
||||
})
|
||||
}
|
||||
if (language !== "Python") return []
|
||||
return collectNodes(root, "call").filter((call) => {
|
||||
const fn = call.childForFieldName("function")
|
||||
return (
|
||||
fn?.type === "attribute" &&
|
||||
fn.childForFieldName("attribute")?.text === target
|
||||
)
|
||||
return fn?.type === "attribute" && fn.childForFieldName("attribute")?.text === target
|
||||
})
|
||||
}
|
||||
|
||||
function evaluateRule(
|
||||
root: Node,
|
||||
rule: AstRule,
|
||||
language: string,
|
||||
): AstResult | null {
|
||||
function evaluateRule(root: Node, rule: AstRule, language: string): AstResult | null {
|
||||
const target = rule.target ?? ""
|
||||
const nodeType = astTargetNodeType(target, language)
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ export function userSubmissionTopic(userId: number) {
|
||||
return `submission:user:${userId}`
|
||||
}
|
||||
|
||||
export async function publishSubmissionUpdate(
|
||||
userId: number,
|
||||
data: SubmissionUpdate,
|
||||
) {
|
||||
export async function publishSubmissionUpdate(userId: number, data: SubmissionUpdate) {
|
||||
const event: SubmissionEvent = { userId, data }
|
||||
await redis.publish(submissionUpdateChannel, JSON.stringify(event))
|
||||
}
|
||||
|
||||
@@ -16,11 +16,7 @@ import { normalizeLanguage } from "@oj2/contract"
|
||||
*
|
||||
* SQL 题不走这里,走 `judge/sql/`;流程图题走 AI 评分。
|
||||
*/
|
||||
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:
|
||||
@@ -92,9 +88,5 @@ export const languageConfigs: Record<string, Record<string, unknown>> = {
|
||||
* 它先过 `normalizeLanguage()`,所以 `Python3` / `Python2` 这类旧值也能命中。
|
||||
*/
|
||||
export function judgeConfigFor(language: string) {
|
||||
return (
|
||||
languageConfigs[language] ??
|
||||
languageConfigs[normalizeLanguage(language) ?? ""] ??
|
||||
null
|
||||
)
|
||||
return languageConfigs[language] ?? languageConfigs[normalizeLanguage(language) ?? ""] ?? null
|
||||
}
|
||||
+32
-112
@@ -40,9 +40,7 @@ interface JudgeResponse {
|
||||
|
||||
function statusValue(value: number): JudgeStatusValue {
|
||||
const statuses = new Set<number>(Object.values(JudgeStatus))
|
||||
return statuses.has(value)
|
||||
? (value as JudgeStatusValue)
|
||||
: JudgeStatus.SYSTEM_ERROR
|
||||
return statuses.has(value) ? (value as JudgeStatusValue) : JudgeStatus.SYSTEM_ERROR
|
||||
}
|
||||
|
||||
function templateForLanguage(value: unknown, language: string) {
|
||||
@@ -73,12 +71,9 @@ async function requestJudge(
|
||||
testCaseId: string,
|
||||
) {
|
||||
const languageConfig = judgeConfigFor(language)
|
||||
if (!languageConfig)
|
||||
throw new Error(`Unsupported judge language: ${language}`)
|
||||
if (!languageConfig) throw new Error(`Unsupported judge language: ${language}`)
|
||||
|
||||
const token = createHash("sha256")
|
||||
.update(config.judgeServerToken)
|
||||
.digest("hex")
|
||||
const token = createHash("sha256").update(config.judgeServerToken).digest("hex")
|
||||
const response = await fetch(new URL("/judge", config.judgeServerUrl), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -126,9 +121,7 @@ async function persistResult(
|
||||
|
||||
if (
|
||||
!currentSubmission ||
|
||||
![JudgeStatus.PENDING, JudgeStatus.JUDGING].includes(
|
||||
currentSubmission.result as 6 | 7,
|
||||
)
|
||||
![JudgeStatus.PENDING, JudgeStatus.JUDGING].includes(currentSubmission.result as 6 | 7)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@@ -178,8 +171,7 @@ async function persistResult(
|
||||
const problems = asRecord(acmStatus[statusKey])
|
||||
const previous = asRecord(problems[String(problemId)])
|
||||
const previousStatus = previous.status
|
||||
const wasAccepted =
|
||||
typeof previousStatus === "number" && isAccepted(previousStatus)
|
||||
const wasAccepted = typeof previousStatus === "number" && isAccepted(previousStatus)
|
||||
const acceptedNow = isAccepted(result)
|
||||
|
||||
if (previousStatus === undefined) {
|
||||
@@ -199,11 +191,9 @@ async function persistResult(
|
||||
await tx
|
||||
.update(schema.userProfile)
|
||||
.set({
|
||||
submissionNumber:
|
||||
profile.submissionNumber + (contestId === null ? 1 : 0),
|
||||
submissionNumber: profile.submissionNumber + (contestId === null ? 1 : 0),
|
||||
acceptedNumber:
|
||||
profile.acceptedNumber +
|
||||
(contestId === null && acceptedNow && !wasAccepted ? 1 : 0),
|
||||
profile.acceptedNumber + (contestId === null && acceptedNow && !wasAccepted ? 1 : 0),
|
||||
acmProblemsStatus: acmStatus,
|
||||
})
|
||||
.where(eq(schema.userProfile.id, profile.id))
|
||||
@@ -227,10 +217,7 @@ async function persistResult(
|
||||
submissionInfo: {},
|
||||
})
|
||||
.onConflictDoNothing({
|
||||
target: [
|
||||
schema.acmContestRank.contestId,
|
||||
schema.acmContestRank.userId,
|
||||
],
|
||||
target: [schema.acmContestRank.contestId, schema.acmContestRank.userId],
|
||||
})
|
||||
|
||||
const [rank] = await tx
|
||||
@@ -254,8 +241,7 @@ async function persistResult(
|
||||
is_ac: acceptedNow,
|
||||
ac_time: 0,
|
||||
error_number:
|
||||
errorNumber +
|
||||
(!acceptedNow && result !== JudgeStatus.COMPILE_ERROR ? 1 : 0),
|
||||
errorNumber + (!acceptedNow && result !== JudgeStatus.COMPILE_ERROR ? 1 : 0),
|
||||
is_first_ac: false,
|
||||
}
|
||||
let totalTime = rank.totalTime
|
||||
@@ -263,11 +249,7 @@ async function persistResult(
|
||||
if (acceptedNow) {
|
||||
const acTime = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(Date.parse(submissionCreateTime) -
|
||||
Date.parse(contest.startTime)) /
|
||||
1000,
|
||||
),
|
||||
Math.floor((Date.parse(submissionCreateTime) - Date.parse(contest.startTime)) / 1000),
|
||||
)
|
||||
nextInfo.ac_time = acTime
|
||||
nextInfo.is_first_ac = problem.acceptedNumber === 0
|
||||
@@ -291,11 +273,7 @@ async function persistResult(
|
||||
})
|
||||
}
|
||||
|
||||
async function markSystemError(
|
||||
submissionId: string,
|
||||
userId: number,
|
||||
error: unknown,
|
||||
) {
|
||||
async function markSystemError(submissionId: string, userId: number, error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
const updated = await db
|
||||
.update(schema.submission)
|
||||
@@ -306,10 +284,7 @@ async function markSystemError(
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.id, submissionId),
|
||||
inArray(schema.submission.result, [
|
||||
JudgeStatus.PENDING,
|
||||
JudgeStatus.JUDGING,
|
||||
]),
|
||||
inArray(schema.submission.result, [JudgeStatus.PENDING, JudgeStatus.JUDGING]),
|
||||
),
|
||||
)
|
||||
.returning({ id: schema.submission.id })
|
||||
@@ -339,10 +314,7 @@ async function markSystemError(
|
||||
* 都不会被它覆盖。唯一能撞上的是「重判刚把状态置回 PENDING,同一刻上一个被遗弃的
|
||||
* 任务才失败」——结果是这次重判被吃掉、显示成系统错误,比静默卡死看得见。
|
||||
*/
|
||||
export async function failAbandonedSubmission(
|
||||
submissionId: string,
|
||||
error: unknown,
|
||||
) {
|
||||
export async function failAbandonedSubmission(submissionId: string, error: unknown) {
|
||||
const [row] = await db
|
||||
.select({ userId: schema.submission.userId })
|
||||
.from(schema.submission)
|
||||
@@ -359,24 +331,12 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
problem: schema.problem,
|
||||
})
|
||||
.from(schema.submission)
|
||||
.innerJoin(
|
||||
schema.problem,
|
||||
eq(schema.submission.problemId, schema.problem.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.id, job.submissionId),
|
||||
eq(schema.problem.id, job.problemId),
|
||||
),
|
||||
)
|
||||
.innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id))
|
||||
.where(and(eq(schema.submission.id, job.submissionId), eq(schema.problem.id, job.problemId)))
|
||||
.limit(1)
|
||||
|
||||
if (!row) throw new Error(`Submission ${job.submissionId} does not exist`)
|
||||
if (
|
||||
![JudgeStatus.PENDING, JudgeStatus.JUDGING].includes(
|
||||
row.submission.result as 6 | 7,
|
||||
)
|
||||
) {
|
||||
if (![JudgeStatus.PENDING, JudgeStatus.JUDGING].includes(row.submission.result as 6 | 7)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -392,10 +352,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
status: "judging",
|
||||
})
|
||||
|
||||
const rawTemplate = templateForLanguage(
|
||||
row.problem.template,
|
||||
row.submission.language,
|
||||
)
|
||||
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}`
|
||||
@@ -421,10 +378,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
if (response.err) {
|
||||
result = JudgeStatus.COMPILE_ERROR
|
||||
statisticInfo = {
|
||||
err_info:
|
||||
typeof response.data === "string"
|
||||
? response.data
|
||||
: JSON.stringify(response.data),
|
||||
err_info: typeof response.data === "string" ? response.data : JSON.stringify(response.data),
|
||||
score: 0,
|
||||
}
|
||||
} else {
|
||||
@@ -435,19 +389,11 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
(left, right) => Number(left.test_case) - Number(right.test_case),
|
||||
)
|
||||
info = { err: null, data: cases }
|
||||
const firstFailure = cases.find(
|
||||
(item) => item.result !== JudgeStatus.ACCEPTED,
|
||||
)
|
||||
const firstFailure = cases.find((item) => item.result !== JudgeStatus.ACCEPTED)
|
||||
result = statusValue(firstFailure?.result ?? JudgeStatus.ACCEPTED)
|
||||
statisticInfo = {
|
||||
time_cost: Math.max(
|
||||
0,
|
||||
...cases.map((item) => Number(item.cpu_time) || 0),
|
||||
),
|
||||
memory_cost: Math.max(
|
||||
0,
|
||||
...cases.map((item) => Number(item.memory) || 0),
|
||||
),
|
||||
time_cost: Math.max(0, ...cases.map((item) => Number(item.cpu_time) || 0)),
|
||||
memory_cost: Math.max(0, ...cases.map((item) => Number(item.memory) || 0)),
|
||||
score: 0,
|
||||
}
|
||||
// SQL 判题给出的中文提示(只读拒绝/超时/内存/无结果集)只存在测试点的
|
||||
@@ -456,20 +402,12 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
const failedMessage = cases.find(
|
||||
(item) => item.result !== JudgeStatus.ACCEPTED && item.error_message,
|
||||
)?.error_message
|
||||
if (typeof failedMessage === "string")
|
||||
statisticInfo.err_info = failedMessage
|
||||
if (typeof failedMessage === "string") statisticInfo.err_info = failedMessage
|
||||
|
||||
if (result === JudgeStatus.ACCEPTED) {
|
||||
const rules = astRulesForLanguage(
|
||||
row.problem.astRules,
|
||||
row.submission.language,
|
||||
)
|
||||
const rules = astRulesForLanguage(row.problem.astRules, row.submission.language)
|
||||
if (rules.length > 0) {
|
||||
const ast = await checkAst(
|
||||
row.submission.code,
|
||||
row.submission.language,
|
||||
rules,
|
||||
)
|
||||
const ast = await checkAst(row.submission.code, row.submission.language, rules)
|
||||
if (!ast.passed) {
|
||||
result = JudgeStatus.AST_CHECK_FAILED
|
||||
statisticInfo.ast_results = ast.results
|
||||
@@ -518,9 +456,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
)
|
||||
}
|
||||
if (updated > 0) {
|
||||
const unlocked = await updateAchievementsForProblemSet(
|
||||
row.submission.userId,
|
||||
)
|
||||
const unlocked = await updateAchievementsForProblemSet(row.submission.userId)
|
||||
await publishAchievementNotification(
|
||||
row.submission.userId,
|
||||
unlocked.map((achievement) => ({
|
||||
@@ -534,10 +470,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to record problem set progress for ${row.submission.id}`,
|
||||
error,
|
||||
)
|
||||
console.error(`Failed to record problem set progress for ${row.submission.id}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,10 +488,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
})),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to update achievements for ${row.submission.id}`,
|
||||
error,
|
||||
)
|
||||
console.error(`Failed to update achievements for ${row.submission.id}`, error)
|
||||
}
|
||||
|
||||
await publishSubmissionUpdate(row.submission.userId, {
|
||||
@@ -566,10 +496,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
submissionId: row.submission.id,
|
||||
result,
|
||||
status: "finished",
|
||||
score:
|
||||
typeof statisticInfo.score === "number"
|
||||
? statisticInfo.score
|
||||
: undefined,
|
||||
score: typeof statisticInfo.score === "number" ? statisticInfo.score : undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to judge submission ${row.submission.id}`, error)
|
||||
@@ -595,22 +522,16 @@ async function judgeSqlSubmission(
|
||||
const refSql = answers
|
||||
.map((item) => asRecord(item))
|
||||
.find(
|
||||
(item) =>
|
||||
item.language === "SQL" &&
|
||||
typeof item.code === "string" &&
|
||||
item.code.trim(),
|
||||
(item) => item.language === "SQL" && typeof item.code === "string" && item.code.trim(),
|
||||
)?.code
|
||||
if (typeof refSql !== "string") throw new Error("题目缺少 SQL 标准答案")
|
||||
|
||||
const info = await readInfo(problem.testCaseId)
|
||||
if (!info) throw new Error("测试点信息读取失败")
|
||||
if (!info.sql)
|
||||
throw new Error("测试点不是 SQL 类型,请重新上传 SQL 测试点压缩包")
|
||||
if (!info.sql) throw new Error("测试点不是 SQL 类型,请重新上传 SQL 测试点压缩包")
|
||||
|
||||
// 按 "1","2",… 的数字序遍历,保证测试点顺序稳定
|
||||
const keys = Object.keys(info.test_cases ?? {}).sort(
|
||||
(a, b) => Number(a) - Number(b),
|
||||
)
|
||||
const keys = Object.keys(info.test_cases ?? {}).sort((a, b) => Number(a) - Number(b))
|
||||
if (keys.length === 0) throw new Error("题目没有任何测试点")
|
||||
|
||||
const cases: JudgeCase[] = []
|
||||
@@ -635,8 +556,7 @@ async function judgeSqlSubmission(
|
||||
})
|
||||
if (!outcome.ok) {
|
||||
// 初始化/标准答案执行失败属出题配置问题,整题 SYSTEM_ERROR
|
||||
if (outcome.result === JudgeStatus.SYSTEM_ERROR)
|
||||
throw new Error(outcome.message)
|
||||
if (outcome.result === JudgeStatus.SYSTEM_ERROR) throw new Error(outcome.message)
|
||||
// 子进程被杀(超时/内存)也走这里,按学生错误记成一个测试点
|
||||
cases.push({
|
||||
test_case: String(index + 1),
|
||||
|
||||
@@ -95,13 +95,10 @@ export async function runSqlChild() {
|
||||
// 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")
|
||||
const memoryish = message.includes("out of memory") || message.includes("Aborted")
|
||||
finish({
|
||||
ok: false,
|
||||
result: memoryish
|
||||
? JudgeStatus.MEMORY_LIMIT_EXCEEDED
|
||||
: JudgeStatus.SYSTEM_ERROR,
|
||||
result: memoryish ? JudgeStatus.MEMORY_LIMIT_EXCEEDED : JudgeStatus.SYSTEM_ERROR,
|
||||
message: memoryish ? "内存超出限制" : message.slice(0, 200),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,11 +45,7 @@ const DISPLAY_ROW_LIMIT = 20
|
||||
const ERROR_MESSAGE_MAX_LEN = 200
|
||||
|
||||
/** prepare 阶段的语法类错误,映射为 COMPILE_ERROR */
|
||||
const SYNTAX_ERROR_MARKERS = [
|
||||
"syntax error",
|
||||
"unrecognized token",
|
||||
"incomplete input",
|
||||
]
|
||||
const SYNTAX_ERROR_MARKERS = ["syntax error", "unrecognized token", "incomplete input"]
|
||||
|
||||
export class SqlCaseError extends Error {
|
||||
constructor(
|
||||
@@ -89,11 +85,9 @@ type Canonical = string
|
||||
*/
|
||||
function canonicalValue(value: unknown): Canonical {
|
||||
if (value === null || value === undefined) return "null"
|
||||
if (value instanceof Uint8Array)
|
||||
return `blob:${Buffer.from(value).toString("hex")}`
|
||||
if (value instanceof Uint8Array) return `blob:${Buffer.from(value).toString("hex")}`
|
||||
if (typeof value === "number") {
|
||||
if (Number.isInteger(value) && Math.abs(value) < 2 ** 53)
|
||||
return `num:${value}`
|
||||
if (Number.isInteger(value) && Math.abs(value) < 2 ** 53) return `num:${value}`
|
||||
// Python 的 format(v, ".6g")
|
||||
return `num:${formatG6(value)}`
|
||||
}
|
||||
@@ -209,17 +203,11 @@ class ByteBudget {
|
||||
? Buffer.byteLength(value)
|
||||
: 8 // 数字和 NULL 按定长算,撑不出内存
|
||||
if (bytes > this.maxBytes) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
"单个数据值超出内存限制",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "单个数据值超出内存限制")
|
||||
}
|
||||
this.used += bytes
|
||||
if (this.used > this.maxBytes) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
"查询结果超出内存限制",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "查询结果超出内存限制")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,10 +277,7 @@ function dumpTables(db: Database, budget?: ByteBudget) {
|
||||
return canonicalRow(row as unknown[])
|
||||
})
|
||||
if (rows.length > ROW_LIMIT) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
`表 ${table} 超过 ${ROW_LIMIT} 行`,
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, `表 ${table} 超过 ${ROW_LIMIT} 行`)
|
||||
}
|
||||
state[String(table)] = {
|
||||
// 空表 exec 不返回结果,列数用 table_info 兜底
|
||||
@@ -317,20 +302,12 @@ function trustedErrorText(message: string) {
|
||||
}
|
||||
|
||||
/** 执行受信脚本(初始化/标准答案),任何失败都是出题问题 → SYSTEM_ERROR */
|
||||
function executeTrusted(
|
||||
db: Database,
|
||||
script: string,
|
||||
deadline: number,
|
||||
prefix: string,
|
||||
) {
|
||||
function executeTrusted(db: Database, script: string, deadline: number, prefix: string) {
|
||||
try {
|
||||
return executeStatements(db, script, deadline)
|
||||
} catch (error) {
|
||||
if (error instanceof SqlCaseError) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.SYSTEM_ERROR,
|
||||
`${prefix}: ${error.detail}`,
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.SYSTEM_ERROR, `${prefix}: ${error.detail}`)
|
||||
}
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.SYSTEM_ERROR,
|
||||
@@ -350,9 +327,7 @@ function runStudent(
|
||||
// 查询题只读:PRAGMA query_only 是 SQLite 原生开关,替代旧实现的 authorizer 白名单
|
||||
if (mode === "query") db.run("PRAGMA query_only=1")
|
||||
// 把题目的 memoryLimit 变成学生看得见的约束,替代旧实现的 setlimit(LIMIT_LENGTH)
|
||||
const budget = new ByteBudget(
|
||||
Math.max(Math.trunc(memoryLimitMb), 1) * 1024 * 1024,
|
||||
)
|
||||
const budget = new ByteBudget(Math.max(Math.trunc(memoryLimitMb), 1) * 1024 * 1024)
|
||||
try {
|
||||
const last = executeStatements(
|
||||
db,
|
||||
@@ -363,10 +338,7 @@ function runStudent(
|
||||
// 就把只读关掉了。旧实现的 authorizer 把 SQLITE_PRAGMA 一律拒掉,这里对齐它。
|
||||
// 教学场景下学生也没有用 PRAGMA 的正当需求,两种题型一律拒。
|
||||
if (leadingKeyword(statement) === "PRAGMA") {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.RUNTIME_ERROR,
|
||||
"禁止使用 PRAGMA 语句",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.RUNTIME_ERROR, "禁止使用 PRAGMA 语句")
|
||||
}
|
||||
// 兜底:万一漏掉某种改设置的写法,限制在每条语句前都重放一遍
|
||||
applyLimits(db, memoryLimitMb)
|
||||
@@ -380,16 +352,10 @@ function runStudent(
|
||||
if (error instanceof SqlCaseError) throw error
|
||||
const message = String((error as Error).message)
|
||||
if (message.includes("interrupted")) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.CPU_TIME_LIMIT_EXCEEDED,
|
||||
"SQL 执行超时",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.CPU_TIME_LIMIT_EXCEEDED, "SQL 执行超时")
|
||||
}
|
||||
if (message.includes("database or disk is full")) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
"数据量超出内存限制",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "数据量超出内存限制")
|
||||
}
|
||||
// WASM 堆触顶(zeroblob/group_concat 构造出的超大单值)或 SQLite 自身的长度上限
|
||||
if (
|
||||
@@ -397,10 +363,7 @@ function runStudent(
|
||||
message.includes("out of memory") ||
|
||||
message.includes("Aborted")
|
||||
) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
"单个数据值超出内存限制",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "单个数据值超出内存限制")
|
||||
}
|
||||
if (message.includes("readonly database")) {
|
||||
throw new SqlCaseError(
|
||||
@@ -423,12 +386,7 @@ function runStudent(
|
||||
}
|
||||
}
|
||||
|
||||
function compare(
|
||||
expected: unknown,
|
||||
actual: unknown,
|
||||
mode: string,
|
||||
orderSensitive: boolean,
|
||||
) {
|
||||
function compare(expected: unknown, actual: unknown, mode: string, orderSensitive: boolean) {
|
||||
if (mode === "query") {
|
||||
const exp = expected as ResultSet
|
||||
const act = actual as ResultSet
|
||||
@@ -493,12 +451,7 @@ export async function runCase(
|
||||
const refDb = newDatabase(SQL, options.memoryLimitMb)
|
||||
try {
|
||||
executeTrusted(refDb, initSql, trustedDeadline, "初始化脚本执行失败")
|
||||
const last = executeTrusted(
|
||||
refDb,
|
||||
refSql,
|
||||
trustedDeadline,
|
||||
"标准答案执行失败",
|
||||
)
|
||||
const last = executeTrusted(refDb, refSql, trustedDeadline, "标准答案执行失败")
|
||||
if (options.mode === "query") {
|
||||
expected = last
|
||||
} else {
|
||||
@@ -600,18 +553,13 @@ function dumpDisplayTables(db: Database, only?: Set<string>): DisplayTable[] {
|
||||
const name = String(raw)
|
||||
if (only && !only.has(name)) continue
|
||||
const quoted = name.replaceAll('"', '""')
|
||||
const columns = (
|
||||
db.exec(`PRAGMA table_info("${quoted}")`)[0]?.values ?? []
|
||||
).map((row) => ({
|
||||
const columns = (db.exec(`PRAGMA table_info("${quoted}")`)[0]?.values ?? []).map((row) => ({
|
||||
name: String(row[1]),
|
||||
type: String(row[2] ?? ""),
|
||||
}))
|
||||
const total = Number(
|
||||
db.exec(`SELECT COUNT(*) FROM "${quoted}"`)[0]?.values[0]?.[0] ?? 0,
|
||||
)
|
||||
const total = Number(db.exec(`SELECT COUNT(*) FROM "${quoted}"`)[0]?.values[0]?.[0] ?? 0)
|
||||
const rows = (
|
||||
db.exec(`SELECT * FROM "${quoted}" LIMIT ${DISPLAY_ROW_LIMIT}`)[0]
|
||||
?.values ?? []
|
||||
db.exec(`SELECT * FROM "${quoted}" LIMIT ${DISPLAY_ROW_LIMIT}`)[0]?.values ?? []
|
||||
).map((row) => (row as unknown[]).map(displayValue))
|
||||
tables.push({
|
||||
name,
|
||||
@@ -666,17 +614,12 @@ export async function buildDisplay(
|
||||
while (statement.step()) {
|
||||
rows.push(statement.get())
|
||||
if (rows.length > ROW_LIMIT) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.SYSTEM_ERROR,
|
||||
`标准答案结果超过 ${ROW_LIMIT} 行`,
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.SYSTEM_ERROR, `标准答案结果超过 ${ROW_LIMIT} 行`)
|
||||
}
|
||||
}
|
||||
expected = {
|
||||
columns: queryResultColumns(names, tables),
|
||||
rows: rows
|
||||
.slice(0, DISPLAY_ROW_LIMIT)
|
||||
.map((row) => row.map(displayValue)),
|
||||
rows: rows.slice(0, DISPLAY_ROW_LIMIT).map((row) => row.map(displayValue)),
|
||||
total_rows: rows.length,
|
||||
truncated: rows.length > DISPLAY_ROW_LIMIT,
|
||||
}
|
||||
@@ -692,10 +635,7 @@ export async function buildDisplay(
|
||||
)
|
||||
}
|
||||
if (expected === null) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.SYSTEM_ERROR,
|
||||
"标准答案未产生查询结果集",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未产生查询结果集")
|
||||
}
|
||||
return { tables, expected }
|
||||
}
|
||||
@@ -704,18 +644,11 @@ export async function buildDisplay(
|
||||
executeTrusted(db, refSql, deadline, "标准答案执行失败")
|
||||
const after = dumpTables(db)
|
||||
const changed = new Set<string>()
|
||||
for (const name of new Set([
|
||||
...Object.keys(before),
|
||||
...Object.keys(after),
|
||||
])) {
|
||||
if (JSON.stringify(before[name]) !== JSON.stringify(after[name]))
|
||||
changed.add(name)
|
||||
for (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
||||
if (JSON.stringify(before[name]) !== JSON.stringify(after[name])) changed.add(name)
|
||||
}
|
||||
if (changed.size === 0) {
|
||||
throw new SqlCaseError(
|
||||
JudgeStatus.SYSTEM_ERROR,
|
||||
"标准答案未修改任何表数据,请检查题目配置",
|
||||
)
|
||||
throw new SqlCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未修改任何表数据,请检查题目配置")
|
||||
}
|
||||
const changedTables = dumpDisplayTables(db, changed)
|
||||
// 被标准答案 DROP 的表已不在库中,用初始展示数据补齐条目(前端据 dropped 提示「表已删除」)
|
||||
|
||||
@@ -92,17 +92,13 @@ const PHASE_FAILURE: Record<string, SqlJobFailure> = {
|
||||
},
|
||||
}
|
||||
|
||||
async function runJob<T>(
|
||||
job: SqlJob,
|
||||
budget: JobBudget,
|
||||
): Promise<SqlJobOutcome<T>> {
|
||||
async function runJob<T>(job: SqlJob, budget: JobBudget): Promise<SqlJobOutcome<T>> {
|
||||
// 递归闸。子进程里绝不允许再 spawn 子进程 —— 见文件头「为什么必须有这道闸」。
|
||||
if (process.env[CHILD_MARKER]) {
|
||||
return {
|
||||
ok: false,
|
||||
result: JudgeStatus.SYSTEM_ERROR,
|
||||
message:
|
||||
"SQL 判题子进程试图再起子进程,已阻断(入口子命令分发可能不正确)",
|
||||
message: "SQL 判题子进程试图再起子进程,已阻断(入口子命令分发可能不正确)",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +119,7 @@ async function runJob<T>(
|
||||
child.stdin.write(JSON.stringify(job))
|
||||
await child.stdin.end()
|
||||
|
||||
let timer = setTimeout(
|
||||
() => child.kill("SIGKILL"),
|
||||
budget.trustedMs + STARTUP_SLACK_MS,
|
||||
)
|
||||
let timer = setTimeout(() => child.kill("SIGKILL"), budget.trustedMs + STARTUP_SLACK_MS)
|
||||
let phase = ""
|
||||
// stderr 要边读边看:阶段标记一到就得马上换兜底时限,攒到进程结束再读就没意义了
|
||||
const readStderr = (async () => {
|
||||
@@ -143,10 +136,7 @@ async function runJob<T>(
|
||||
phase = latest
|
||||
if (phase === "student" && budget.studentMs !== null) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(
|
||||
() => child.kill("SIGKILL"),
|
||||
budget.studentMs + STUDENT_SLACK_MS,
|
||||
)
|
||||
timer = setTimeout(() => child.kill("SIGKILL"), budget.studentMs + STUDENT_SLACK_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,10 +144,7 @@ async function runJob<T>(
|
||||
|
||||
let stdout = ""
|
||||
try {
|
||||
;[stdout] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
readStderr,
|
||||
])
|
||||
;[stdout] = await Promise.all([new Response(child.stdout).text(), readStderr])
|
||||
await child.exited
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
@@ -197,11 +184,7 @@ export function runSqlCase(job: Extract<SqlJob, { kind: "judge" }>) {
|
||||
})
|
||||
}
|
||||
|
||||
export function buildSqlDisplay(
|
||||
initSql: string,
|
||||
refSql: string,
|
||||
mode: "query" | "modify",
|
||||
) {
|
||||
export function buildSqlDisplay(initSql: string, refSql: string, mode: "query" | "modify") {
|
||||
// 子进程产出的形状由 engine.ts 的 dumpDisplayTables / runDisplay 决定,就是契约里的
|
||||
// SqlDisplay —— 同一个仓库里的两端,不在这儿再 parse 一遍
|
||||
return runJob<SqlDisplay>(
|
||||
|
||||
@@ -5,9 +5,7 @@ import { JudgeStatus, type JudgeStatusValue } from "@oj2/contract"
|
||||
export { JudgeStatus, type JudgeStatusValue }
|
||||
|
||||
export function isAccepted(result: number) {
|
||||
return (
|
||||
result === JudgeStatus.ACCEPTED || result === JudgeStatus.AST_CHECK_FAILED
|
||||
)
|
||||
return result === JudgeStatus.ACCEPTED || result === JudgeStatus.AST_CHECK_FAILED
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,10 +37,7 @@ export function judgeStatusName(result: number) {
|
||||
* 它们从分母里摘掉 —— 否则全班同时交卷的那几秒,分母涨了分子没涨,正确率凭空掉一截。
|
||||
* 人数口径不受影响:交了但还在判的学生仍然算「交过」,不该被点名成「没做」。
|
||||
*/
|
||||
export const UNJUDGED_RESULTS: JudgeStatusValue[] = [
|
||||
JudgeStatus.PENDING,
|
||||
JudgeStatus.JUDGING,
|
||||
]
|
||||
export const UNJUDGED_RESULTS: JudgeStatusValue[] = [JudgeStatus.PENDING, JudgeStatus.JUDGING]
|
||||
|
||||
/**
|
||||
* **不**计入「这道题失败了几次」的状态。除了通过(含 AST_CHECK_FAILED,那也是答案对了)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export function parseProblemTemplate(template: string) {
|
||||
const section = (name: string) =>
|
||||
template.match(
|
||||
new RegExp(`//${name} BEGIN\\n([\\s\\S]+?)//${name} END`),
|
||||
)?.[1] ?? ""
|
||||
template.match(new RegExp(`//${name} BEGIN\\n([\\s\\S]+?)//${name} END`))?.[1] ?? ""
|
||||
|
||||
return {
|
||||
prepend: section("PREPEND"),
|
||||
|
||||
Reference in new issue
Block a user