diff --git a/packages/hydrojudge/src/flow.ts b/packages/hydrojudge/src/flow.ts new file mode 100644 index 000000000..0fa5a1b0b --- /dev/null +++ b/packages/hydrojudge/src/flow.ts @@ -0,0 +1,94 @@ +import Queue from 'p-queue'; +import { STATUS } from '@hydrooj/utils/lib/status'; +import { getConfig } from './config'; +import { CompileError, FormatError } from './error'; +import { Context, ContextSubTask } from './judge/interface'; +import { NormalizedCase, NormalizedSubtask } from './utils'; + +interface Task { + compile: () => Promise; + judgeCase: (c: NormalizedCase, sid: string) => ( + (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => Promise + ) +} + +function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase']) { + return async (ctx: Context) => { + subtask.type = subtask.type || 'min'; + const ctxSubtask = { + subtask, + status: 0, + score: subtask.type === 'min' + ? subtask.score + : 0, + }; + const cases = []; + for (const cid in subtask.cases) { + const runner = judgeCase(subtask.cases[cid], subtask.id.toString() ?? sid); + cases.push(ctx.queue.add(async () => { + if (ctx.errored + || (ctxSubtask.subtask.type === 'min' && ctxSubtask.score === 0) + || (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score) + || ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length) + ) { + ctx.next({ + case: { + id: subtask.cases[cid].id, + status: STATUS.STATUS_CANCELED, + subtaskId: subtask.id, + score: 0, + time: 0, + memory: 0, + message: '', + }, + addProgress: 100 / ctx.config.count, + }); + } else await runner(ctx, ctxSubtask, runner); + })); + } + await Promise.all(cases).catch((e) => { + ctx.errored = true; + throw e; + }); + ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); + return ctxSubtask.score; + }; +} + +export const runFlow = async (ctx: Context, task: Task) => { + if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); + ctx.next({ status: STATUS.STATUS_COMPILING }); + if (ctx.config.template && 'content' in ctx.code) { + if (ctx.config.template[ctx.lang]) { + const tpl = ctx.config.template[ctx.lang]; + ctx.code.content = tpl[0] + ctx.code.content + tpl[1]; + } else throw new CompileError('Language not supported by provided templates'); + } + await task.compile(); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const tasks = []; + ctx.total_status = 0; + ctx.total_score = 0; + ctx.total_memory_usage_kb = 0; + ctx.total_time_usage_ms = 0; + ctx.rerun = getConfig('rerun') || 0; + ctx.queue = new Queue({ concurrency: getConfig('singleTaskParallelism') }); + ctx.failed = {}; + for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid, task.judgeCase)(ctx)); + const scores = await Promise.all(tasks); + for (const sid in ctx.config.subtasks) { + let effective = true; + for (const required of ctx.config.subtasks[sid].if || []) { + if (ctx.failed[required.toString()]) effective = false; + } + if (effective) ctx.total_score += scores[sid]; + } + ctx.stat.done = new Date(); + if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) }); + ctx.end({ + status: ctx.total_status, + score: ctx.total_score, + time: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000, + memory: ctx.total_memory_usage_kb, + }); +}; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index c6e5a9198..a8ad2c57a 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -1,13 +1,11 @@ -import Queue from 'p-queue'; import { STATUS } from '@hydrooj/utils/lib/status'; import { check, compileChecker } from '../check'; import compile from '../compile'; -import { getConfig } from '../config'; -import { CompileError, FormatError } from '../error'; +import { runFlow } from '../flow'; import { Logger } from '../log'; import { CmdFile, del, run } from '../sandbox'; import signals from '../signals'; -import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils'; +import { NormalizedCase, parseFilename } from '../utils'; import { Context, ContextSubTask } from './interface'; const Score = { @@ -20,25 +18,6 @@ const logger = new Logger('judge/default'); function judgeCase(c: NormalizedCase, sid: string) { return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => { - if (ctx.errored - || (ctxSubtask.subtask.type === 'min' && ctxSubtask.score === 0) - || (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score) - || ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length) - ) { - ctx.next({ - case: { - id: c.id, - status: STATUS.STATUS_CANCELED, - subtaskId: ctxSubtask.subtask.id, - score: 0, - time: 0, - memory: 0, - message: '', - }, - addProgress: 100 / ctx.config.count, - }); - return; - } const { filename } = ctx.config; const copyIn = { ...ctx.execute.copyIn }; if (filename) copyIn[`${filename}.in`] = c.input ? { src: c.input } : { content: '' }; @@ -140,84 +119,29 @@ function judgeCase(c: NormalizedCase, sid: string) { }; } -function judgeSubtask(subtask: NormalizedSubtask, sid: string) { - return async (ctx: Context) => { - subtask.type = subtask.type || 'min'; - const ctxSubtask = { - subtask, - status: 0, - score: subtask.type === 'min' - ? subtask.score - : 0, - }; - const cases = []; - for (const cid in subtask.cases) { - const runner = judgeCase(subtask.cases[cid], subtask.id.toString() ?? sid); - cases.push(ctx.queue.add(() => runner(ctx, ctxSubtask, runner))); - } - await Promise.all(cases).catch((e) => { - ctx.errored = true; - throw e; - }); - ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); - return ctxSubtask.score; - }; -} - -export const judge = async (ctx: Context) => { - if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); - ctx.next({ status: STATUS.STATUS_COMPILING }); - if (ctx.config.template && 'content' in ctx.code) { - if (ctx.config.template[ctx.lang]) { - const tpl = ctx.config.template[ctx.lang]; - ctx.code.content = tpl[0] + ctx.code.content + tpl[1]; - } else throw new CompileError('Language not supported by provided templates'); - } - [ctx.execute, ctx.checker] = await Promise.all([ - compile( - ctx.session.getLang(ctx.lang), ctx.code, - Object.fromEntries( - (ctx.config.user_extra_files || []).map((i) => [i.split('/').pop(), { src: i }]), - ), - ctx.next, - ), - compileChecker( - ctx.session.getLang, - ctx.config.checker_type, - ctx.config.checker, - { - user_code: ctx.code, - ...Object.fromEntries( - (ctx.config.judge_extra_files || []).map((i) => [parseFilename(i), { src: i }]), +export const judge = async (ctx: Context) => await runFlow(ctx, { + compile: async () => { + [ctx.execute, ctx.checker] = await Promise.all([ + compile( + ctx.session.getLang(ctx.lang), ctx.code, + Object.fromEntries( + (ctx.config.user_extra_files || []).map((i) => [i.split('/').pop(), { src: i }]), ), - }, - ), - ]); - ctx.clean.push(ctx.execute.clean, ctx.checker.clean); - ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); - const tasks = []; - ctx.total_status = 0; - ctx.total_score = 0; - ctx.total_memory_usage_kb = 0; - ctx.total_time_usage_ms = 0; - ctx.rerun = getConfig('rerun') || 0; - ctx.queue = new Queue({ concurrency: getConfig('singleTaskParallelism') }); - ctx.failed = {}; - for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx)); - const scores = await Promise.all(tasks); - for (const sid in ctx.config.subtasks) { - let effective = true; - for (const required of ctx.config.subtasks[sid].if || []) { - if (ctx.failed[required.toString()]) effective = false; - } - if (effective) ctx.total_score += scores[sid]; - } - ctx.stat.done = new Date(); - if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) }); - ctx.end({ - status: ctx.total_status, - score: ctx.total_score, - time: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000, - memory: ctx.total_memory_usage_kb, - }); -}; + ctx.next, + ), + compileChecker( + ctx.session.getLang, + ctx.config.checker_type, + ctx.config.checker, + { + user_code: ctx.code, + ...Object.fromEntries( + (ctx.config.judge_extra_files || []).map((i) => [parseFilename(i), { src: i }]), + ), + }, + ), + ]); + ctx.clean.push(ctx.execute.clean, ctx.checker.clean); + }, + judgeCase, +}); diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index f2c71fc06..8289ac5e2 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -1,12 +1,11 @@ -import Queue from 'p-queue'; import { STATUS } from '@hydrooj/utils/lib/status'; import compile from '../compile'; -import { getConfig } from '../config'; +import { runFlow } from '../flow'; import { runPiped } from '../sandbox'; import signals from '../signals'; import { parse } from '../testlib'; import { - findFileSync, NormalizedCase, NormalizedSubtask, parseFilename, + findFileSync, NormalizedCase, parseFilename, } from '../utils'; import { Context, ContextSubTask } from './interface'; @@ -76,66 +75,32 @@ function judgeCase(c: NormalizedCase) { }; } -function judgeSubtask(subtask: NormalizedSubtask) { - return async (ctx: Context) => { - subtask.type = subtask.type || 'min'; - const ctxSubtask = { - subtask, - status: 0, - score: subtask.type === 'min' - ? subtask.score - : 0, - }; - const cases = []; - for (const cid in subtask.cases) { - cases.push(ctx.queue.add(() => judgeCase(subtask.cases[cid])(ctx, ctxSubtask))); - } - await Promise.all(cases); - ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); - ctx.total_score += ctxSubtask.score; - }; -} - -export const judge = async (ctx: Context) => { - ctx.next({ status: STATUS.STATUS_COMPILING }); - [ctx.executeUser, ctx.executeInteractor] = await Promise.all([ - (() => { - const copyIn = {}; - for (const file of ctx.config.user_extra_files) { - copyIn[parseFilename(file)] = { src: file }; - } - return compile(ctx.session.getLang(ctx.lang), ctx.code, copyIn, ctx.next); - })(), - (() => { - const copyIn = { - 'testlib.h': { src: testlibSrc }, - user_code: ctx.code, - }; - for (const file of ctx.config.judge_extra_files) { - copyIn[parseFilename(file)] = { src: file }; - } - return compile( - ctx.session.getLang(parseFilename(ctx.config.interactor).split('.')[1].replace('@', '.')), - { src: ctx.config.interactor }, - copyIn, - ); - })(), - ]); - ctx.clean.push(ctx.executeUser.clean, ctx.executeInteractor.clean); - ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); - const tasks = []; - ctx.total_status = ctx.total_score = ctx.total_memory_usage_kb = ctx.total_time_usage_ms = 0; - ctx.queue = new Queue({ concurrency: getConfig('parallelism') }); - for (const sid in ctx.config.subtasks) { - tasks.push(judgeSubtask(ctx.config.subtasks[sid])(ctx)); - } - await Promise.all(tasks); - ctx.stat.done = new Date(); - if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) }); - ctx.end({ - status: ctx.total_status, - score: ctx.total_score, - time: ctx.total_time_usage_ms, - memory: ctx.total_memory_usage_kb, - }); -}; +export const judge = async (ctx: Context) => await runFlow(ctx, { + compile: async () => { + [ctx.executeUser, ctx.executeInteractor] = await Promise.all([ + (() => { + const copyIn = {}; + for (const file of ctx.config.user_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return compile(ctx.session.getLang(ctx.lang), ctx.code, copyIn, ctx.next); + })(), + (() => { + const copyIn = { + 'testlib.h': { src: testlibSrc }, + user_code: ctx.code, + }; + for (const file of ctx.config.judge_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return compile( + ctx.session.getLang(parseFilename(ctx.config.interactor).split('.')[1].replace('@', '.')), + { src: ctx.config.interactor }, + copyIn, + ); + })(), + ]); + ctx.clean.push(ctx.executeUser.clean, ctx.executeInteractor.clean); + }, + judgeCase, +}); diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts index 6e273321e..006ca208a 100644 --- a/packages/hydrojudge/src/judge/submit_answer.ts +++ b/packages/hydrojudge/src/judge/submit_answer.ts @@ -1,11 +1,9 @@ import { readFile } from 'fs-extra'; -import Queue from 'p-queue'; import { STATUS } from '@hydrooj/utils/lib/status'; import { check, compileChecker } from '../check'; -import { getConfig } from '../config'; -import { FormatError } from '../error'; +import { runFlow } from '../flow'; import { del, run } from '../sandbox'; -import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils'; +import { NormalizedCase, parseFilename } from '../utils'; import { Context, ContextSubTask } from './interface'; const Score = { @@ -16,23 +14,6 @@ const Score = { function judgeCase(c: NormalizedCase, sid: string) { return async (ctx: Context, ctxSubtask: ContextSubTask) => { - if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min') - || (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score) - || ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length) - ) { - ctx.next({ - case: { - id: c.id, - status: STATUS.STATUS_CANCELED, - score: 0, - time: 0, - memory: 0, - message: '', - }, - addProgress: 100 / ctx.config.count, - }); - return; - } const chars = /[a-zA-Z0-9_.-]/; const name = (ctx.config.filename && /^[a-zA-Z0-9-_#.]+$/.test(ctx.config.filename)) ? ctx.config.filename.replace('#', c.id.toString()) @@ -96,67 +77,23 @@ function judgeCase(c: NormalizedCase, sid: string) { }; } -function judgeSubtask(subtask: NormalizedSubtask, sid: string) { - return async (ctx: Context) => { - subtask.type = subtask.type || 'min'; - const ctxSubtask = { - subtask, - status: 0, - score: subtask.type === 'min' - ? subtask.score - : 0, - }; - const cases = []; - for (const cid in subtask.cases) { - const runner = judgeCase(subtask.cases[cid], sid); - cases.push(ctx.queue.add(() => runner(ctx, ctxSubtask))); - } - await Promise.all(cases).catch((e) => { - ctx.errored = true; - throw e; - }); - ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); - return ctxSubtask.score; - }; -} - export const judge = async (ctx: Context) => { - if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); - ctx.next({ status: STATUS.STATUS_COMPILING }); - ctx.checker = await (() => { - const copyIn = { user_code: ctx.code }; - for (const file of ctx.config.judge_extra_files) { - copyIn[parseFilename(file)] = { src: file }; - } - return compileChecker( - ctx.session.getLang, - ctx.config.checker_type, - ctx.config.checker, - copyIn, - ); - })(); - ctx.clean.push(ctx.checker.clean); - ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); - const tasks = []; - ctx.total_status = 0; - ctx.total_score = 0; - ctx.queue = new Queue({ concurrency: getConfig('parallelism') }); - ctx.failed = {}; - for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx)); - const scores = await Promise.all(tasks); - for (const sid in ctx.config.subtasks) { - let effective = true; - for (const required of ctx.config.subtasks[sid].if || []) { - if (ctx.failed[required.toString()]) effective = false; - } - if (effective) ctx.total_score += scores[sid]; - } - ctx.stat.done = new Date(); - if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) }); - ctx.end({ - status: ctx.total_status, - score: ctx.total_score, - time: 0, - memory: 0, + await runFlow(ctx, { + compile: async () => { + ctx.checker = await (() => { + const copyIn = { user_code: ctx.code }; + for (const file of ctx.config.judge_extra_files) { + copyIn[parseFilename(file)] = { src: file }; + } + return compileChecker( + ctx.session.getLang, + ctx.config.checker_type, + ctx.config.checker, + copyIn, + ); + })(); + ctx.clean.push(ctx.checker.clean); + }, + judgeCase, }); };