judge: handle optional stdin and redirection

This commit is contained in:
undefined committed 2022-09-06 13:02:46 +08:00
1 parent 5dfb81c6c3
commit 9d0aeebe12
5 files changed
+59 -72

No files matched your search

+11 -17
View File
@@ -5,7 +5,7 @@ import compile, { compileChecker } from '../compile';
import { runFlow } from '../flow'; import { runFlow } from '../flow';
import { Execute } from '../interface'; import { Execute } from '../interface';
import { Logger } from '../log'; import { Logger } from '../log';
import { CmdFile, del, run } from '../sandbox'; import { del, run } from '../sandbox';
import signals from '../signals'; import signals from '../signals';
import { NormalizedCase } from '../utils'; import { NormalizedCase } from '../utils';
import { Context, ContextSubTask } from './interface'; import { Context, ContextSubTask } from './interface';
@@ -14,27 +14,21 @@ const logger = new Logger('judge/default');
function judgeCase(c: NormalizedCase) { function judgeCase(c: NormalizedCase) {
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => { return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
const { filename } = ctx.config;
const copyIn = { ...ctx.execute.copyIn };
if (filename) copyIn[`${filename}.in`] = c.input ? { src: c.input } : { content: '' };
const copyOutCached = filename ? [`${filename}.out?`] : [];
const stdin = filename ? null : { src: c.input };
const res = await run( const res = await run(
ctx.execute.execute, ctx.execute.execute,
{ {
stdin, stdin: { src: c.input },
copyIn, copyIn: ctx.execute.copyIn,
copyOutCached, filename: ctx.config.filename,
time: c.time * ctx.execute.time, time: c.time * ctx.execute.time,
memory: c.memory, memory: c.memory,
cacheStdoutAndStderr: true, cacheStdoutAndStderr: true,
}, },
); );
const { code, time, memory } = res; const {
code, time, memory, fileIds,
} = res;
let { status } = res; let { status } = res;
let stdout: CmdFile = { fileId: res.fileIds[filename ? `${filename}.out` : 'stdout'] };
const stderr = { fileId: res.fileIds['stderr'] };
if (!stdout.fileId) stdout = { content: '' };
let message: any = ''; let message: any = '';
let score = 0; let score = 0;
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
@@ -48,8 +42,8 @@ function judgeCase(c: NormalizedCase) {
copyIn: ctx.checker.copyIn || {}, copyIn: ctx.checker.copyIn || {},
input: { src: c.input }, input: { src: c.input },
output: { src: c.output }, output: { src: c.output },
user_stdout: stdout, user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' },
user_stderr: stderr, user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' },
score: c.score, score: c.score,
detail: ctx.config.detail ?? true, detail: ctx.config.detail ?? true,
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
@@ -73,8 +67,8 @@ function judgeCase(c: NormalizedCase) {
try { try {
const r = await run(langConfig.analysis, { const r = await run(langConfig.analysis, {
copyIn: { copyIn: {
...copyIn, ...ctx.execute.copyIn,
input: stdin || { content: '' }, input: { src: c.input },
[langConfig.code_file || 'foo']: ctx.code, [langConfig.code_file || 'foo']: ctx.code,
compile: { content: langConfig.compile || '' }, compile: { content: langConfig.compile || '' },
execute: { content: langConfig.execute || '' }, execute: { content: langConfig.execute || '' },
+6 -13
View File
@@ -4,7 +4,7 @@ import { STATUS } from '@hydrooj/utils/lib/status';
import checkers from '../checkers'; import checkers from '../checkers';
import compile, { compileChecker, compileValidator } from '../compile'; import compile, { compileChecker, compileValidator } from '../compile';
import { Execute } from '../interface'; import { Execute } from '../interface';
import { CmdFile, del, run } from '../sandbox'; import { del, run } from '../sandbox';
import signals from '../signals'; import signals from '../signals';
import { parseMemoryMB, parseTimeMS } from '../utils'; import { parseMemoryMB, parseTimeMS } from '../utils';
import { Context } from './interface'; import { Context } from './interface';
@@ -35,29 +35,22 @@ export async function judge(ctx: Context) {
memory: parseMemoryMB(ctx.config.memory || '256m'), memory: parseMemoryMB(ctx.config.memory || '256m'),
}, },
); );
console.log(validateResult);
if (validateResult.status !== STATUS.STATUS_ACCEPTED) { if (validateResult.status !== STATUS.STATUS_ACCEPTED) {
const message = `${validateResult.stdout || ''}\n${validateResult.stderr || ''}`.trim(); const message = `${validateResult.stdout || ''}\n${validateResult.stderr || ''}`.trim();
return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message }); return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message });
} }
const copyIn = { ...execute.copyIn };
const { filename } = ctx.config;
if (filename) copyIn[`${filename}.in`] = { src: input };
const res = await run( const res = await run(
execute.execute, execute.execute,
{ {
stdin: filename ? null : { src: input }, stdin: { src: input },
copyIn, copyIn: execute.copyIn,
copyOutCached: [filename ? `${filename}.out?` : 'stdout', 'stderr'],
time: parseTimeMS(ctx.config.time || '1s') * execute.time, time: parseTimeMS(ctx.config.time || '1s') * execute.time,
memory: parseMemoryMB(ctx.config.memory || '256m'), memory: parseMemoryMB(ctx.config.memory || '256m'),
cacheStdoutAndStderr: true,
}, },
); );
const { code, time, memory } = res; const { code, time, memory } = res;
let { status } = res; let { status } = res;
let stdout: CmdFile = { fileId: res.fileIds[filename ? `${filename}.out` : 'stdout'] };
console.log(res);
if (!stdout.fileId) stdout = { content: '' };
let message: any = ''; let message: any = '';
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
if (time > ctx.config.time * execute.time) { if (time > ctx.config.time * execute.time) {
@@ -70,8 +63,8 @@ export async function judge(ctx: Context) {
copyIn: checker.copyIn || {}, copyIn: checker.copyIn || {},
input: { src: input }, input: { src: input },
output: { content: '' }, output: { content: '' },
user_stdout: stdout, user_stdout: res.fileIds.stdout ? { fileId: res.fileIds.stdout } : { content: '' },
user_stderr: { fileId: res.fileIds['stderr'] }, user_stderr: { fileId: res.fileIds.stderr },
score: 100, score: 100,
detail: ctx.config.detail ?? true, detail: ctx.config.detail ?? true,
env: { ...ctx.env, HYDRO_TESTCASE: '0' }, env: { ...ctx.env, HYDRO_TESTCASE: '0' },
+4 -14
View File
@@ -1,5 +1,3 @@
import path from 'path';
import fs from 'fs-extra';
import { STATUS } from '@hydrooj/utils/lib/status'; import { STATUS } from '@hydrooj/utils/lib/status';
import compile from '../compile'; import compile from '../compile';
import { CompileError } from '../error'; import { CompileError } from '../error';
@@ -48,18 +46,11 @@ export const judge = async (ctx: Context) => {
} }
ctx.clean.push(ctx.execute.clean); ctx.clean.push(ctx.execute.clean);
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
const copyIn = { ...ctx.execute.copyIn };
const { filename } = ctx.config;
if (filename) copyIn[`${filename}.in`] = { content: ctx.input };
const copyOut = filename ? [`${filename}.out?`] : [];
const stdout = path.resolve(ctx.tmpdir, '0.out');
const res = await run( const res = await run(
ctx.execute.execute, ctx.execute.execute,
{ {
stdin: filename ? null : { content: ctx.input }, stdin: { content: ctx.input },
stdout: filename ? null : stdout, copyIn: ctx.execute.copyIn,
copyIn,
copyOut,
// Allow 2x limits for better debugging // Allow 2x limits for better debugging
time: parseTimeMS(ctx.config.time || '1s') * ctx.execute.time * 2, time: parseTimeMS(ctx.config.time || '1s') * ctx.execute.time * 2,
memory: parseMemoryMB(ctx.config.memory || '128m'), memory: parseMemoryMB(ctx.config.memory || '128m'),
@@ -67,7 +58,6 @@ export const judge = async (ctx: Context) => {
); );
const { code, time, memory } = res; const { code, time, memory } = res;
let { status } = res; let { status } = res;
if (!fs.existsSync(stdout)) fs.writeFileSync(stdout, '');
const message: string[] = []; const message: string[] = [];
if (time > parseTimeMS(ctx.config.time || '1s') * ctx.execute.time) { if (time > parseTimeMS(ctx.config.time || '1s') * ctx.execute.time) {
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
@@ -78,7 +68,7 @@ export const judge = async (ctx: Context) => {
if (code < 32) message.push(`ExitCode: ${code} (${signals[code]})`); if (code < 32) message.push(`ExitCode: ${code} (${signals[code]})`);
else message.push(`ExitCode: ${code}`); else message.push(`ExitCode: ${code}`);
} }
message.push(fs.readFileSync(stdout).toString()); message.push(res.stdout);
message.push(res.stderr); message.push(res.stderr);
ctx.next({ ctx.next({
status, status,
@@ -98,7 +88,7 @@ export const judge = async (ctx: Context) => {
ctx.analysis = true; ctx.analysis = true;
const r = await run(langConfig.analysis, { const r = await run(langConfig.analysis, {
copyIn: { copyIn: {
...copyIn, ...ctx.execute.copyIn,
input: { content: ctx.input }, input: { content: ctx.input },
[langConfig.code_file || 'foo']: ctx.code, [langConfig.code_file || 'foo']: ctx.code,
compile: { content: langConfig.compile || '' }, compile: { content: langConfig.compile || '' },
+25 -27
View File
@@ -6,7 +6,7 @@ import { STATUS } from '@hydrooj/utils/lib/status';
import { getConfig } from './config'; import { getConfig } from './config';
import { FormatError, SystemError } from './error'; import { FormatError, SystemError } from './error';
import { Logger } from './log'; import { Logger } from './log';
import { SandboxClient } from './sandbox/client'; import client from './sandbox/client';
import { import {
Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus, Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus,
} from './sandbox/interface'; } from './sandbox/interface';
@@ -41,6 +41,8 @@ interface Parameter {
copyOutCached?: string[]; copyOutCached?: string[];
cacheStdoutAndStderr?: boolean; cacheStdoutAndStderr?: boolean;
env?: Record<string, string>; env?: Record<string, string>;
/** redirect stdin & stdout */
filename?: string;
} }
interface SandboxAdaptedResult { interface SandboxAdaptedResult {
@@ -69,29 +71,24 @@ function parseArgs(execute: string): string[] {
return args; return args;
} }
function proc({ function proc(params: Parameter): Cmd {
execute = '', const copyOut = supportOptional
time = 16000, ? params.copyOut
memory = parseMemoryMB(getConfig('memoryMax')), : params.copyOut.map((i) => (i.endsWith('?') ? i.substring(0, i.length - 1) : i));
processLimit = getConfig('processLimit'),
stdin = { content: '' }, copyIn = {}, copyOut = [], copyOutCached = [],
cacheStdoutAndStderr = false,
env = {},
}: Parameter = {}): Cmd {
if (!supportOptional) {
copyOut = copyOut.map((i) => (i.endsWith('?') ? i.substring(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]; const copyOutCached = [...(params.copyOutCached || [])];
if (cacheStdoutAndStderr) { if (params.cacheStdoutAndStderr) copyOutCached.push(params.filename ? `${params.filename}.out` : 'stdout', 'stderr');
copyOutCachedCopy.push('stdout', 'stderr'); const copyIn = { ...(params.copyIn || {}) };
} const stdin = params.stdin || { content: '' };
if (params.filename) copyIn[`${params.filename}.in`] = stdin;
const time = params.time || 16000;
const memory = params.memory || parseMemoryMB(getConfig('memoryMax'));
return { return {
args: parseArgs(execute), args: parseArgs(params.execute || ''),
env: [...getConfig('env').split('\n'), ...Object.keys(env).map((i) => `${i}=${env[i].replace(/=/g, '\\=')}`)], env: [...getConfig('env').split('\n'), ...Object.entries(params.env || {}).map(([k, v]) => `${k}=${v.replace(/=/g, '\\=')}`)],
files: [ files: [
stdin, params.filename ? { content: '' } : stdin,
{ name: 'stdout', max: Math.floor(1024 * 1024 * size) }, { name: 'stdout', max: Math.floor(1024 * 1024 * size) },
{ name: 'stderr', max: Math.floor(1024 * 1024 * size) }, { name: 'stderr', max: Math.floor(1024 * 1024 * size) },
], ],
@@ -100,10 +97,10 @@ function proc({
memoryLimit: Math.floor(memory * 1024 * 1024), memoryLimit: Math.floor(memory * 1024 * 1024),
strictMemoryLimit: getConfig('strict_memory'), strictMemoryLimit: getConfig('strict_memory'),
// stackLimit: memory * 1024 * 1024, // stackLimit: memory * 1024 * 1024,
procLimit: processLimit, procLimit: params.processLimit || getConfig('processLimit'),
copyIn, copyIn,
copyOut, copyOut,
copyOutCached: copyOutCachedCopy, copyOutCached,
}; };
} }
@@ -120,10 +117,12 @@ async function adaptResult(result: SandboxResult, params: Parameter): Promise<Sa
if (ret.time >= (params.time || 16000)) { if (ret.time >= (params.time || 16000)) {
ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
} }
const outname = params.filename ? `${params.filename}.out` : 'stdout';
ret.files = result.files || {}; ret.files = result.files || {};
ret.fileIds = result.fileIds || {}; ret.fileIds = result.fileIds || {};
if (params.stdout) await fs.writeFile(params.stdout, ret.files.stdout || ''); if (ret.fileIds[outname]) ret.fileIds.stdout = ret.fileIds[outname];
else ret.stdout = ret.files.stdout || ''; if (params.stdout) await fs.writeFile(params.stdout, ret.files[outname] || '');
else ret.stdout = ret.files[outname] || '';
if (params.stderr) await fs.writeFile(params.stderr, ret.files.stderr || result.error || ''); if (params.stderr) await fs.writeFile(params.stderr, ret.files.stderr || result.error || '');
else ret.stderr = ret.files.stderr || result.error || ''; else ret.stderr = ret.files.stderr || result.error || '';
if (result.error) ret.error = result.error; if (result.error) ret.error = result.error;
@@ -159,7 +158,7 @@ export async function runPiped(execute0: Parameter, execute1: Parameter): Promis
body.cmd[1].files[1] = null; body.cmd[1].files[1] = null;
const id = callId++; const id = callId++;
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(body)); if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(body));
res = await new SandboxClient(getConfig('sandbox_host')).run(body); res = await client.run(body);
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res)); if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res));
} catch (e) { } catch (e) {
if (e instanceof FormatError || e instanceof SystemError) throw e; if (e instanceof FormatError || e instanceof SystemError) throw e;
@@ -170,13 +169,12 @@ export async function runPiped(execute0: Parameter, execute1: Parameter): Promis
} }
export async function del(fileId: string) { export async function del(fileId: string) {
await new SandboxClient(getConfig('sandbox_host')).deleteFile(fileId); await client.deleteFile(fileId);
} }
export async function run(execute: string, params?: Parameter): Promise<SandboxAdaptedResult> { export async function run(execute: string, params?: Parameter): Promise<SandboxAdaptedResult> {
let result: SandboxResult; let result: SandboxResult;
try { try {
const client = new SandboxClient(getConfig('sandbox_host'));
if (!supportOptional) { if (!supportOptional) {
const res = await client.version(); const res = await client.version();
supportOptional = res.copyOutOptional; supportOptional = res.copyOutOptional;
+13 -1
View File
@@ -1,7 +1,8 @@
import superagent from 'superagent'; import superagent from 'superagent';
import { getConfig } from '../config';
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface'; import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
export class SandboxClient { class SandboxClient {
constructor(public baseURL: string) { constructor(public baseURL: string) {
if (baseURL.endsWith('/')) this.baseURL = baseURL.substring(0, baseURL.length - 1); if (baseURL.endsWith('/')) this.baseURL = baseURL.substring(0, baseURL.length - 1);
} }
@@ -26,3 +27,14 @@ export class SandboxClient {
return superagent.get(`${this.baseURL}/version`).then((res) => res.body); return superagent.get(`${this.baseURL}/version`).then((res) => res.body);
} }
} }
const client = new Proxy({} as SandboxClient, {
get(self, key) {
let url = getConfig('sandbox_host');
if (url.endsWith('/')) url = url.substring(0, url.length - 1);
const c = new SandboxClient(url);
return c[key].bind(c);
},
});
export default client;