judge: support binary output (#301)

This commit is contained in:
Yang Gao authored and GitHub committed 2022-02-05 15:36:23 +08:00
1 parent 24cd356f78
commit 2c01eaa967
8 files changed
+89 -72

No files matched your search

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