From 9d0aeebe1283195ec464ce9aae11bd96c6c63339 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 5 Sep 2022 11:12:42 +0800 Subject: [PATCH] judge: handle optional stdin and redirection --- packages/hydrojudge/src/judge/default.ts | 28 +++++------- packages/hydrojudge/src/judge/hack.ts | 19 +++------ packages/hydrojudge/src/judge/run.ts | 18 ++------ packages/hydrojudge/src/sandbox.ts | 52 +++++++++++------------ packages/hydrojudge/src/sandbox/client.ts | 14 +++++- 5 files changed, 59 insertions(+), 72 deletions(-) diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index d2620b904..d803126c6 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -5,7 +5,7 @@ import compile, { compileChecker } from '../compile'; import { runFlow } from '../flow'; import { Execute } from '../interface'; import { Logger } from '../log'; -import { CmdFile, del, run } from '../sandbox'; +import { del, run } from '../sandbox'; import signals from '../signals'; import { NormalizedCase } from '../utils'; import { Context, ContextSubTask } from './interface'; @@ -14,27 +14,21 @@ const logger = new Logger('judge/default'); function judgeCase(c: NormalizedCase) { 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( ctx.execute.execute, { - stdin, - copyIn, - copyOutCached, + stdin: { src: c.input }, + copyIn: ctx.execute.copyIn, + filename: ctx.config.filename, time: c.time * ctx.execute.time, memory: c.memory, cacheStdoutAndStderr: true, }, ); - const { code, time, memory } = res; + const { + code, time, memory, fileIds, + } = 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 score = 0; if (status === STATUS.STATUS_ACCEPTED) { @@ -48,8 +42,8 @@ function judgeCase(c: NormalizedCase) { copyIn: ctx.checker.copyIn || {}, input: { src: c.input }, output: { src: c.output }, - user_stdout: stdout, - user_stderr: stderr, + user_stdout: fileIds.stdout ? { fileId: fileIds.stdout } : { content: '' }, + user_stderr: fileIds.stderr ? { fileId: fileIds.stderr } : { content: '' }, score: c.score, detail: ctx.config.detail ?? true, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, @@ -73,8 +67,8 @@ function judgeCase(c: NormalizedCase) { try { const r = await run(langConfig.analysis, { copyIn: { - ...copyIn, - input: stdin || { content: '' }, + ...ctx.execute.copyIn, + input: { src: c.input }, [langConfig.code_file || 'foo']: ctx.code, compile: { content: langConfig.compile || '' }, execute: { content: langConfig.execute || '' }, diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index d09ee69c3..9df9a91f8 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -4,7 +4,7 @@ import { STATUS } from '@hydrooj/utils/lib/status'; import checkers from '../checkers'; import compile, { compileChecker, compileValidator } from '../compile'; import { Execute } from '../interface'; -import { CmdFile, del, run } from '../sandbox'; +import { del, run } from '../sandbox'; import signals from '../signals'; import { parseMemoryMB, parseTimeMS } from '../utils'; import { Context } from './interface'; @@ -35,29 +35,22 @@ export async function judge(ctx: Context) { memory: parseMemoryMB(ctx.config.memory || '256m'), }, ); - console.log(validateResult); if (validateResult.status !== STATUS.STATUS_ACCEPTED) { const message = `${validateResult.stdout || ''}\n${validateResult.stderr || ''}`.trim(); 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( execute.execute, { - stdin: filename ? null : { src: input }, - copyIn, - copyOutCached: [filename ? `${filename}.out?` : 'stdout', 'stderr'], + stdin: { src: input }, + copyIn: execute.copyIn, time: parseTimeMS(ctx.config.time || '1s') * execute.time, memory: parseMemoryMB(ctx.config.memory || '256m'), + cacheStdoutAndStderr: true, }, ); const { code, time, memory } = 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 = ''; if (status === STATUS.STATUS_ACCEPTED) { if (time > ctx.config.time * execute.time) { @@ -70,8 +63,8 @@ export async function judge(ctx: Context) { copyIn: checker.copyIn || {}, input: { src: input }, output: { content: '' }, - user_stdout: stdout, - user_stderr: { fileId: res.fileIds['stderr'] }, + user_stdout: res.fileIds.stdout ? { fileId: res.fileIds.stdout } : { content: '' }, + user_stderr: { fileId: res.fileIds.stderr }, score: 100, detail: ctx.config.detail ?? true, env: { ...ctx.env, HYDRO_TESTCASE: '0' }, diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 766ec3ac8..495ae963c 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -1,5 +1,3 @@ -import path from 'path'; -import fs from 'fs-extra'; import { STATUS } from '@hydrooj/utils/lib/status'; import compile from '../compile'; import { CompileError } from '../error'; @@ -48,18 +46,11 @@ export const judge = async (ctx: Context) => { } ctx.clean.push(ctx.execute.clean); 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( ctx.execute.execute, { - stdin: filename ? null : { content: ctx.input }, - stdout: filename ? null : stdout, - copyIn, - copyOut, + stdin: { content: ctx.input }, + copyIn: ctx.execute.copyIn, // Allow 2x limits for better debugging time: parseTimeMS(ctx.config.time || '1s') * ctx.execute.time * 2, memory: parseMemoryMB(ctx.config.memory || '128m'), @@ -67,7 +58,6 @@ export const judge = async (ctx: Context) => { ); const { code, time, memory } = res; let { status } = res; - if (!fs.existsSync(stdout)) fs.writeFileSync(stdout, ''); const message: string[] = []; if (time > parseTimeMS(ctx.config.time || '1s') * ctx.execute.time) { 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]})`); else message.push(`ExitCode: ${code}`); } - message.push(fs.readFileSync(stdout).toString()); + message.push(res.stdout); message.push(res.stderr); ctx.next({ status, @@ -98,7 +88,7 @@ export const judge = async (ctx: Context) => { ctx.analysis = true; const r = await run(langConfig.analysis, { copyIn: { - ...copyIn, + ...ctx.execute.copyIn, input: { content: ctx.input }, [langConfig.code_file || 'foo']: ctx.code, compile: { content: langConfig.compile || '' }, diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 8fa3daee7..f1fe501b2 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -6,7 +6,7 @@ import { STATUS } from '@hydrooj/utils/lib/status'; import { getConfig } from './config'; import { FormatError, SystemError } from './error'; import { Logger } from './log'; -import { SandboxClient } from './sandbox/client'; +import client from './sandbox/client'; import { Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus, } from './sandbox/interface'; @@ -41,6 +41,8 @@ interface Parameter { copyOutCached?: string[]; cacheStdoutAndStderr?: boolean; env?: Record; + /** redirect stdin & stdout */ + filename?: string; } interface SandboxAdaptedResult { @@ -69,29 +71,24 @@ function parseArgs(execute: string): string[] { return args; } -function proc({ - execute = '', - time = 16000, - memory = parseMemoryMB(getConfig('memoryMax')), - 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)); - } +function proc(params: Parameter): Cmd { + const copyOut = supportOptional + ? params.copyOut + : params.copyOut.map((i) => (i.endsWith('?') ? i.substring(0, i.length - 1) : i)); const size = parseMemoryMB(getConfig('stdio_size')); const rate = getConfig('rate'); - const copyOutCachedCopy = [...copyOutCached]; - if (cacheStdoutAndStderr) { - copyOutCachedCopy.push('stdout', 'stderr'); - } + const copyOutCached = [...(params.copyOutCached || [])]; + if (params.cacheStdoutAndStderr) copyOutCached.push(params.filename ? `${params.filename}.out` : '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 { - args: parseArgs(execute), - env: [...getConfig('env').split('\n'), ...Object.keys(env).map((i) => `${i}=${env[i].replace(/=/g, '\\=')}`)], + args: parseArgs(params.execute || ''), + env: [...getConfig('env').split('\n'), ...Object.entries(params.env || {}).map(([k, v]) => `${k}=${v.replace(/=/g, '\\=')}`)], files: [ - stdin, + params.filename ? { content: '' } : stdin, { name: 'stdout', 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), strictMemoryLimit: getConfig('strict_memory'), // stackLimit: memory * 1024 * 1024, - procLimit: processLimit, + procLimit: params.processLimit || getConfig('processLimit'), copyIn, copyOut, - copyOutCached: copyOutCachedCopy, + copyOutCached, }; } @@ -120,10 +117,12 @@ async function adaptResult(result: SandboxResult, params: Parameter): Promise= (params.time || 16000)) { ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } + const outname = params.filename ? `${params.filename}.out` : 'stdout'; ret.files = result.files || {}; ret.fileIds = result.fileIds || {}; - if (params.stdout) await fs.writeFile(params.stdout, ret.files.stdout || ''); - else ret.stdout = ret.files.stdout || ''; + if (ret.fileIds[outname]) ret.fileIds.stdout = ret.fileIds[outname]; + 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 || ''); else ret.stderr = ret.files.stderr || 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; const id = callId++; 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)); } catch (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) { - await new SandboxClient(getConfig('sandbox_host')).deleteFile(fileId); + await client.deleteFile(fileId); } export async function run(execute: string, params?: Parameter): Promise { let result: SandboxResult; try { - const client = new SandboxClient(getConfig('sandbox_host')); if (!supportOptional) { const res = await client.version(); supportOptional = res.copyOutOptional; diff --git a/packages/hydrojudge/src/sandbox/client.ts b/packages/hydrojudge/src/sandbox/client.ts index 2c9554c7f..2cb81ce5e 100644 --- a/packages/hydrojudge/src/sandbox/client.ts +++ b/packages/hydrojudge/src/sandbox/client.ts @@ -1,7 +1,8 @@ import superagent from 'superagent'; +import { getConfig } from '../config'; import { SandboxRequest, SandboxResult, SandboxVersion } from './interface'; -export class SandboxClient { +class SandboxClient { constructor(public baseURL: string) { 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); } } + +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;