diff --git a/packages/hydrojudge/src/checkers.ts b/packages/hydrojudge/src/checkers.ts index 8617cbdad..92e9dca60 100644 --- a/packages/hydrojudge/src/checkers.ts +++ b/packages/hydrojudge/src/checkers.ts @@ -16,11 +16,16 @@ export interface CheckConfig { env?: Record; } +export interface NextPass extends AsyncDisposable { + input: CopyInFile; + state?: Record; +} + type Checker = (config: CheckConfig) => Promise<{ status: number; score: number; message: string | { message: string, params?: any[] }; - nextPass?: { input: CopyInFile, state?: Record }; + nextPass?: NextPass; }>; function parseDiffMsg(msg: string) { @@ -237,7 +242,7 @@ const checkers: Record = new Proxy({ }, async testlib(config) { - const { stderr, status, code, fileIds } = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, { + const res = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, { copyIn: { in: config.input, user_out: config.user_stdout, @@ -248,12 +253,17 @@ const checkers: Record = new Proxy({ env: config.env, copyOutCached: ['nextpass.in?', 'state.txt?'], }); + const cleanup = res[Symbol.asyncDispose]; + const { + stderr, status, code, fileIds, + } = res; if ([STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_TIME_LIMIT_EXCEEDED, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED].includes(status)) { const message = { [STATUS.STATUS_SYSTEM_ERROR]: stderr, [STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Checker Time Limit Exceeded', [STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Checker Memory Limit Exceeded', }[status]; + await cleanup(); return { status: STATUS.STATUS_SYSTEM_ERROR, score: 0, @@ -261,6 +271,7 @@ const checkers: Record = new Proxy({ }; } if (status === STATUS.STATUS_RUNTIME_ERROR && !stderr?.trim()) { + await cleanup(); return { status: STATUS.STATUS_SYSTEM_ERROR, score: 0, @@ -274,15 +285,17 @@ const checkers: Record = new Proxy({ nextPass: { input: { fileId: fileIds['nextpass.in'] }, state: fileIds['state.txt'] ? { 'state.txt': { fileId: fileIds['state.txt'] } } : undefined, + [Symbol.asyncDispose]: cleanup, }, }; } + await cleanup(); return result; }, // https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator async kattis(config) { - const { files, fileIds, code } = await runQueued(`${config.execute} input answer_file feedback_dir`, { + const res = await runQueued(`${config.execute} input answer_file feedback_dir`, { copyIn: { input: config.input, answer_file: config.output, @@ -301,7 +314,8 @@ const checkers: Record = new Proxy({ 'feedback_dir/state.txt?', ], }); - + const cleanup = res[Symbol.asyncDispose]; + const { files, fileIds, code } = res; const status = code === 42 ? STATUS.STATUS_ACCEPTED : code === 43 @@ -328,10 +342,12 @@ const checkers: Record = new Proxy({ state: fileIds['feedback_dir/state.txt'] ? { 'feedback_dir/state.txt': { fileId: fileIds['feedback_dir/state.txt'] } } : undefined, + [Symbol.asyncDispose]: cleanup, }, }; } + await cleanup(); return { status, score, message }; }, }, { diff --git a/packages/hydrojudge/src/judge/communication.ts b/packages/hydrojudge/src/judge/communication.ts index d3bb197b1..fb1a830a4 100644 --- a/packages/hydrojudge/src/judge/communication.ts +++ b/packages/hydrojudge/src/judge/communication.ts @@ -39,7 +39,7 @@ function judgeCase(c: NormalizedCase) { }); } execute[0].execute += managerArgs; - const res = await runPiped(execute, pipeMapping, undefined, `judgeCase[${c.id}]<${ctx.rid}>`); + await using res = await runPiped(execute, pipeMapping, undefined, `judgeCase[${c.id}]<${ctx.rid}>`); const resManager = res[0]; let time = 0; let memory = 0; diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index d3e735a88..470942b07 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -1,5 +1,5 @@ import { NormalizedCase, STATUS } from '@hydrooj/common'; -import checkers from '../checkers'; +import checkers, { type NextPass } from '../checkers'; import { runFlow } from '../flow'; import { runQueued } from '../sandbox'; import signals from '../signals'; @@ -31,7 +31,7 @@ function judgeCase(c: NormalizedCase) { let { status } = res; let message: any = ''; let score = 0; - let nextPass: any; + let nextPass: NextPass | undefined; if (status === STATUS.STATUS_ACCEPTED) { if (time > c.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; @@ -69,9 +69,10 @@ function judgeCase(c: NormalizedCase) { if (mp.i && typeof message === 'string') message = `${message} [Pass ${mp.i}]`; } if (nextPass) { + await using ownedNextPass = nextPass; if (mp.i < ctx.config.multi_pass) { - mp.input = nextPass.input; - mp.state = nextPass.state ?? undefined; + mp.input = ownedNextPass.input; + mp.state = ownedNextPass.state ?? undefined; mp.i++; return await runner(ctx, ctxSubtask, runner); } diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 892c470ad..aeeefda16 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -11,9 +11,7 @@ function judgeCase(c: NormalizedCase) { const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang); if (ctx.config.multi_pass && !mp.i) mp.i = 1; - const [{ - code, signalled, time, memory, - }, resInteractor] = await runPiped([ + await using results = await runPiped([ { execute: ctx.executeUser.execute, copyIn: { ...ctx.executeUser.copyIn, ...mp.state }, @@ -44,6 +42,9 @@ function judgeCase(c: NormalizedCase) { { in: { index: 0, fd: 1 }, out: { index: 1, fd: 0 }, name: 'userToInteractor' }, { in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 }, name: 'interactorToUser' }, ], undefined, `judgeCase[${c.id}]${mp.i ? `[pass=${mp.i}]` : ''}<${ctx.rid}>`); + const [{ + code, signalled, time, memory, + }, resInteractor] = results; // TODO handle tout (maybe pass to checker?) let status: number; let score = 0; diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 23a840d4d..5dda67a86 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -62,6 +62,8 @@ interface SandboxAdaptedResult { error?: string; } +type DisposableSandboxResults = SandboxAdaptedResult[] & AsyncDisposable; + function checkStringArray(args: ParseEntry[]): args is string[] { return args.every((arg: ParseEntry) => typeof arg === 'string'); } @@ -147,9 +149,13 @@ function adaptResult(result: SandboxResult, params: Parameter): SandboxAdaptedRe return ret; } +export async function del(fileId: string) { + await client.deleteFile(fileId); +} + export async function runPiped( execute: Parameter[], pipeMapping: Pick[], params: Parameter = {}, trace: string = '', -): Promise { +): Promise { let res: SandboxResult[]; const size = parseMemoryMB(getConfig('stdio_size')); try { @@ -179,11 +185,10 @@ export async function runPiped( console.error(e); throw new SystemError('Sandbox Error', [e]); } - return res.map((r) => adaptResult(r, params)) as SandboxAdaptedResult[]; -} - -export async function del(fileId: string) { - await client.deleteFile(fileId); + const result = res.map((r) => adaptResult(r, params)) as DisposableSandboxResults; + const fileIds = new Set(result.flatMap((item) => Object.values(item.fileIds || {}))); + (result as any)[Symbol.asyncDispose] = () => Promise.allSettled([...fileIds].map(del)); + return result; } export async function get(fileId: string, dest?: string) { @@ -210,7 +215,7 @@ export function runQueued( return queue.add(async () => { const res = await runPiped(execute, pipeMapping, params, trace); const ret = single ? res[0] : res; - (ret as any)[Symbol.asyncDispose] = () => Promise.allSettled(res.flatMap((t) => Object.values(t.fileIds || {}).map(del))); + (ret as any)[Symbol.asyncDispose] = res[Symbol.asyncDispose]; return ret; }, { priority }); }