diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts index 531a4d8c7..67acf02de 100644 --- a/packages/hydrojudge/src/check.ts +++ b/packages/hydrojudge/src/check.ts @@ -11,12 +11,12 @@ const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'); interface CheckConfig { checker_type: string; - stdin: string, - stdout: string, - user_stdout: string, - user_stderr: string, - score: number, + stdin: CopyInFile, + stdout: CopyInFile, + user_stdout: CopyInFile, + user_stderr: CopyInFile, copyIn?: Record, + score: number, detail: boolean, } diff --git a/packages/hydrojudge/src/checkers.ts b/packages/hydrojudge/src/checkers.ts index add334112..dbb0ad571 100644 --- a/packages/hydrojudge/src/checkers.ts +++ b/packages/hydrojudge/src/checkers.ts @@ -6,12 +6,12 @@ import { CopyInFile } from './sandbox/interface'; import { parse } from './testlib'; interface CheckConfig { - input: string, - output: string, - user_stdout: string, - user_stderr: string, - score: number, + input: CopyInFile, + output: CopyInFile, + user_stdout: CopyInFile, + user_stderr: CopyInFile, copyIn: Record, + score: number, detail: boolean, } @@ -28,8 +28,8 @@ const checkers: Record = { async default(config) { const { stdout } = await run('/usr/bin/diff -BZ usrout answer', { copyIn: { - usrout: { src: config.user_stdout }, - answer: { src: config.output }, + usrout: config.user_stdout, + answer: config.output, ...config.copyIn, }, }); @@ -41,22 +41,24 @@ const checkers: Record = { try { const pt = stdout.split('---'); const u = pt[0].split('\n')[1]; - let usr = u.substr(2, u.length - 2).trim().split(' '); + const usr = u.substring(2).trim().split(' '); const t = pt[1].split('\n')[1]; - let std = t.substr(2, t.length - 2).trim().split(' '); + const std = t.substring(2).trim().split(' '); if (usr.length < std.length) message = 'Standard answer longer than user output.'; else if (usr.length > std.length) message = 'User output longer than standard answer.'; else { + let usrString = usr[0]; + let stdString = std[0]; for (const i in usr) { if (usr[i] !== std[i]) { - usr = usr[i]; - std = std[i]; + usrString = usr[i]; + stdString = std[i]; break; } } - if (usr.length > 20) usr = `${usr.substring(0, 16)}...`; - if (std.length > 20) std = `${std.substring(0, 16)}...`; - message = { message: 'Read {0}, expect {1}.', params: [usr, std] }; + if (usr.length > 20) usrString = `${usrString.substring(0, 16)}...`; + if (std.length > 20) stdString = `${stdString.substring(0, 16)}...`; + message = { message: 'Read {0}, expect {1}.', params: [usrString, stdString] }; } } catch (e) { message = stdout.substring(0, stdout.length - 1 <= 30 ? stdout.length - 1 : 30); @@ -74,8 +76,8 @@ const checkers: Record = { async strict(config) { const { stdout } = await run('/usr/bin/diff usrout answer', { copyIn: { - usrout: { src: config.user_stdout }, - answer: { src: config.output }, + usrout: config.user_stdout, + answer: config.output, ...config.copyIn, }, }); @@ -96,9 +98,9 @@ const checkers: Record = { async hustoj(config) { const { code, stdout } = await run('${dir}/checker input answer usrout', { copyIn: { - usrout: { src: config.user_stdout }, - answer: { src: config.output }, - input: { src: config.input }, + usrout: config.user_stdout, + answer: config.output, + input: config.input, ...config.copyIn, }, }); @@ -121,9 +123,9 @@ const checkers: Record = { async lemon(config) { const { files } = await run(`\${dir}/checker input usrout answer ${config.score} score message`, { copyIn: { - usrout: { src: config.user_stdout }, - answer: { src: config.output }, - input: { src: config.input }, + usrout: config.user_stdout, + answer: config.output, + input: config.input, ...config.copyIn, }, copyOut: ['score', 'message'], @@ -147,8 +149,8 @@ const checkers: Record = { async qduoj(config) { const { status, stdout } = await run('${dir}/checker input usrout', { copyIn: { - usrout: { src: config.user_stdout }, - input: { src: config.input }, + usrout: config.user_stdout, + input: config.input, ...config.copyIn, }, }); @@ -173,9 +175,9 @@ const checkers: Record = { // eslint-disable-next-line prefer-const let { status, stdout, stderr } = await run('${dir}/checker', { copyIn: { - input: { src: config.input }, - user_out: { src: config.user_stdout }, - answer: { src: config.output }, + input: config.input, + user_out: config.user_stdout, + answer: config.output, code: { content: '' }, ...config.copyIn, }, @@ -189,9 +191,9 @@ const checkers: Record = { async testlib(config) { const { stderr, status } = await run('${dir}/checker ${dir}/in ${dir}/user_out ${dir}/answer', { copyIn: { - in: { src: config.input }, - user_out: { src: config.user_stdout }, - answer: { src: config.output }, + in: config.input, + user_out: config.user_stdout, + answer: config.output, ...config.copyIn, }, }); diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index dd61edc03..7fa45905b 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -3,10 +3,11 @@ import { STATUS } from '@hydrooj/utils/lib/status'; import { CompileError } from './error'; import { Execute } from './interface'; import { del, run } from './sandbox'; +import { CopyInFile } from './sandbox/interface'; import { compilerText } from './utils'; export = async function compile( - lang: LangConfig, code: string, target: string, copyIn: any = {}, next?: Function, + lang: LangConfig, code: string, target: string, copyIn: Record = {}, next?: Function, ): Promise { target = lang.target || target; copyIn[lang.code_file] = { content: code }; diff --git a/packages/hydrojudge/src/interface.ts b/packages/hydrojudge/src/interface.ts index 99c7fc4e8..64cd54a09 100644 --- a/packages/hydrojudge/src/interface.ts +++ b/packages/hydrojudge/src/interface.ts @@ -8,5 +8,5 @@ export interface Execute { export interface CompileErrorInfo { stdout?: string, stderr?: string, - status?: string + status?: number } diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index d3a863a00..d00c875f3 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -1,12 +1,10 @@ -import path from 'path'; -import fs from 'fs-extra'; 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 { run } from '../sandbox'; +import { del, run } from '../sandbox'; import signals from '../signals'; import { parseFilename } from '../utils'; import { @@ -40,26 +38,25 @@ function judgeCase(c: Case, sid: string) { const { filename } = ctx.config; const copyIn = { ...ctx.execute.copyIn }; if (filename) copyIn[`${filename}.in`] = c.input ? { src: c.input } : { content: '' }; - const copyOut = filename ? [`${filename}.out?`] : []; + const copyOutCached = filename ? [`${filename}.out?`] : []; const stdin = filename ? null : c.input; - const stdout = path.resolve(ctx.tmpdir, `${c.id}.out`); - const stderr = path.resolve(ctx.tmpdir, `${c.id}.err`); const res = await run( ctx.execute.execute.replace(/\$\{name\}/g, 'code'), { stdin, - stdout: filename ? null : stdout, - stderr, copyIn, - copyOut, + copyOutCached, time: ctxSubtask.subtask.time * ctx.execute.time, memory: ctxSubtask.subtask.memory, + cacheStdoutAndStderr: true, }, ); const { code, time_usage_ms, memory_usage_kb } = res; let { status } = res; - if (res.files[`${filename}.out`] || !fs.existsSync(stdout)) { - fs.writeFileSync(stdout, res.files[`${filename}.out`] || ''); + let stdout = { fileId: res.fileIds['stdout'] }; + const stderr = { fileId: res.fileIds['stderr'] }; + if (res.fileIds[`${filename}.out`]) { + stdout = { fileId: res.fileIds[`${filename}.out`] }; } let message: any = ''; let score = 0; @@ -71,8 +68,8 @@ function judgeCase(c: Case, sid: string) { } else { [status, score, message] = await check({ copyIn: ctx.checker.copyIn, - stdin: c.input, - stdout: c.output, + stdin: { src: c.input }, + stdout: { src: c.output }, user_stdout: stdout, user_stderr: stderr, checker_type: ctx.config.checker_type, @@ -84,10 +81,9 @@ function judgeCase(c: Case, sid: string) { if (code < 32) message = signals[code]; else message = { message: 'Your program returned {0}.', params: [code] }; } - await Promise.all([ - fs.remove(stdout), - fs.remove(stderr), - ]).catch(() => { /* Ignore file doesn't exist */ }); + await Promise.all( + Object.values(res.fileIds).map((id) => del(id)), + ).catch(() => { /* Ignore file doesn't exist */ }); if (runner && ctx.rerun && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) { ctx.rerun--; await runner(ctx, ctxSubtask); diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index 1bd074025..d93176f3e 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -97,10 +97,10 @@ export const judge = async (ctx: Context) => { } else { [status, , message] = await check({ copyIn: copyInDir(path.resolve(ctx.tmpdir, 'checker')), - stdin: input, - stdout, - user_stdout: stdout, - user_stderr: stderr, + stdin: { src: input }, + stdout: { src: stdout }, + user_stdout: { src: stdout }, + user_stderr: { src: stderr }, checker_type: ctx.config.checker_type, score: 100, detail: ctx.config.detail, diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index f5e72c826..b3fe19a12 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -3,7 +3,7 @@ import Queue from 'p-queue'; import { STATUS } from '@hydrooj/utils/lib/status'; import compile from '../compile'; import { getConfig } from '../config'; -import { run } from '../sandbox'; +import { runPiped } from '../sandbox'; import signals from '../signals'; import { parse } from '../testlib'; import { findFileSync, parseFilename } from '../utils'; @@ -22,20 +22,21 @@ function judgeCase(c: Case) { return async (ctx: Context, ctxSubtask: ContextSubTask) => { ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' }; ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' }; - const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await run([ + const [{ code, time_usage_ms, memory_usage_kb }, resInteractor] = await runPiped( { execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'), copyIn: ctx.executeUser.copyIn, time: ctxSubtask.subtask.time * ctx.executeUser.time, memory: ctxSubtask.subtask.memory, - }, { + }, + { execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout /w/out`, copyIn: ctx.executeInteractor.copyIn, time: ctxSubtask.subtask.time * 2 * ctx.executeInteractor.time, memory: ctxSubtask.subtask.memory * 2, copyOut: ['/w/tout?'], }, - ]); + ); // 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 f022904fa..70ae22366 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -38,6 +38,19 @@ interface Parameter { copyIn?: Record; copyOut?: string[]; copyOutCached?: string[]; + cacheStdoutAndStderr?: boolean; +} + +interface SandboxAdaptedResult { + status: number; + code: number; + time_usage_ms: number; + memory_usage_kb?: number; + files: Record; + fileIds?: Record; + stdout?: string; + stderr?: string; + error?: string; } function checkStringArray(args: ParseEntry[]): args is string[] { @@ -58,12 +71,17 @@ function proc({ memory = parseMemoryMB(getConfig('memoryMax')), processLimit = getConfig('processLimit'), stdin = '', copyIn = {}, copyOut = [], copyOutCached = [], + cacheStdoutAndStderr = false, }: Parameter = {}): Cmd { if (!supportOptional) { copyOut = (copyOut as string[]).map((i) => (i.endsWith('?') ? i.substr(0, i.length - 1) : i)); } const size = parseMemoryMB(getConfig('stdio_size')); const rate = getConfig('rate'); + const copyOutCachedCopy = [...copyOutCached]; + if (cacheStdoutAndStderr) { + copyOutCachedCopy.push('stdout', 'stderr'); + } return { args: parseArgs(execute), env: getConfig('env').split('\n'), @@ -80,14 +98,14 @@ function proc({ procLimit: processLimit, copyIn, copyOut, - copyOutCached, + copyOutCached: copyOutCachedCopy, }; } -async function adaptResult(result: SandboxResult, params: Parameter) { - const rate = getConfig('rate'); +async function adaptResult(result: SandboxResult, params: Parameter): Promise { + const rate = getConfig('rate') as number; // FIXME: Signalled? - const ret: any = { + const ret: SandboxAdaptedResult = { status: statusMap.get(result.status) || STATUS.STATUS_ACCEPTED, time_usage_ms: result.time / 1000000 / rate, memory_usage_kb: result.memory / 1024, @@ -107,14 +125,14 @@ async function adaptResult(result: SandboxResult, params: Parameter) { return ret; } -export async function runMultiple(execute: Parameter[]) { +export async function runPiped(execute0: Parameter, execute1: Parameter): Promise<[SandboxAdaptedResult, SandboxAdaptedResult]> { let res: SandboxResult[]; const size = parseMemoryMB(getConfig('stdio_size')); try { const body = { cmd: [ - proc(execute[0]), - proc(execute[1]), + proc(execute0), + proc(execute1), ], pipeMapping: [{ in: { index: 0, fd: 1 }, @@ -142,16 +160,15 @@ export async function runMultiple(execute: Parameter[]) { if (e instanceof FormatError) throw e; throw new SystemError('Sandbox Error', [e]); } - return await Promise.all(res.map((r) => adaptResult(r, {}))); + return await Promise.all(res.map((r) => adaptResult(r, {}))) as [SandboxAdaptedResult, SandboxAdaptedResult]; } export async function del(fileId: string) { await new SandboxClient(getConfig('sandbox_host')).deleteFile(fileId); } -export async function run(execute: string | Parameter[], params?: Parameter) { +export async function run(execute: string, params?: Parameter): Promise { let result: SandboxResult; - if (typeof execute === 'object') return await runMultiple(execute); try { const client = new SandboxClient(getConfig('sandbox_host')); if (!supportOptional) {