diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index 940bd6d35..b9c84d738 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -15,11 +15,14 @@ function isValidConfig(config) { if (config.type !== 'objective' && config.count > (getConfig('testcases_max') || 100)) { throw new FormatError('Too many testcases. Cancelled.'); } - const time = Math.sum(...config.subtasks.flatMap((subtask) => subtask.cases.map((c) => c.time))); + const time = (config.num_processes || 1) * Math.sum(...config.subtasks.flatMap((subtask) => subtask.cases.map((c) => c.time))); if (time > (getConfig('total_time_limit') || 60) * 1000) { throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]); } const memMax = Math.max(...config.subtasks.flatMap((subtask) => subtask.cases.map((c) => c.memory))); + if (config.type === 'communication' && (config.num_processes || 2) > getConfig('processLimit')) { + throw new FormatError('Number of processes larger than processLimit'); + } if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max'); if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) { throw new FormatError('You did not specify a checker.'); diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index e72122879..a70b3fce6 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -51,7 +51,7 @@ const testlibFile = { }; export async function compileLocalFile( - src: string, type: 'checker' | 'validator' | 'interactor' | 'generator' | 'std', + src: string, type: 'checker' | 'validator' | 'interactor' | 'generator' | 'manager' | 'std', getLang, copyIn: CopyIn, withTestlib = true, next?: any, ) { const s = src.replace('@', '.').split('.'); diff --git a/packages/hydrojudge/src/judge/communication.ts b/packages/hydrojudge/src/judge/communication.ts new file mode 100644 index 000000000..1618bfb6a --- /dev/null +++ b/packages/hydrojudge/src/judge/communication.ts @@ -0,0 +1,91 @@ +import { STATUS } from '@hydrooj/utils/lib/status'; +import { runFlow } from '../flow'; +import { Parameter, runPiped } from '../sandbox'; +import signals from '../signals'; +import { NormalizedCase } from '../utils'; +import { Context, ContextSubTask } from './interface'; + +function judgeCase(c: NormalizedCase) { + return async (ctx: Context, ctxSubtask: ContextSubTask) => { + const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang); + let managerArgs = ''; + const execute: Parameter[] = [{ + execute: ctx.executeManager.execute, + stdin: c.input ? { src: c.input } : { content: '' }, + copyIn: ctx.executeManager.copyIn, + time: c.time * 2, + memory: c.memory * 2, + env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, + }]; + const pipeMapping = []; + for (let i = 0; i < ctx.config.num_processes; i++) { + managerArgs += ` /proc/self/fd/${i * 2 + 3} /proc/self/fd/${i * 2 + 4}`; + execute.push({ + execute: `${ctx.executeUser.execute} ${i}`, + copyIn: ctx.executeUser.copyIn, + time: c.time, + memory: c.memory, + addressSpaceLimit: address_space_limit, + processLimit: process_limit, + }); + pipeMapping.push({ + name: `sol2mgr[${i}]`, + in: { index: i + 1, fd: 1 }, + out: { index: 0, fd: i * 2 + 3 }, + }); + pipeMapping.push({ + name: `mgr2sol[${i}]`, + in: { index: 0, fd: i * 2 + 4 }, + out: { index: i + 1, fd: 0 }, + }); + } + execute[0].execute += managerArgs; + const res = await runPiped(execute, pipeMapping); + const resManager = res[0]; + let time = 0; + let memory = 0; + let score = 0; + let status = STATUS.STATUS_ACCEPTED; + let message: any; + const detail = ctx.config.detail ?? true; + for (let i = 0; i < ctx.config.num_processes; i++) { + const result = res[i + 1]; + time += result.time; + memory = Math.max(memory, result.memory); + if (result.time > c.time) status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + else if (result.memory > c.memory * 1024) status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + else if ((result.code && result.code !== 13 /* Broken Pipe */) || (result.code === 13 && !resManager.code)) { + status = STATUS.STATUS_RUNTIME_ERROR; + if (detail) { + if (result.code < 32 && result.signalled) message = signals[result.code]; + else message = { message: 'Your program returned {0}.', params: [result.code] }; + } + } + } + if (status === STATUS.STATUS_ACCEPTED) { + score = Math.floor(c.score * (+resManager.stdout || 0)); + status = score === c.score ? STATUS.STATUS_ACCEPTED : STATUS.STATUS_WRONG_ANSWER; + message = resManager.stderr; + if (resManager.code) message += ` (Manager exited with code ${resManager.code})`; + } + return { + id: c.id, + subtaskId: ctxSubtask.subtask.id, + status, + score, + time, + memory, + message, + }; + }; +} + +export const judge = async (ctx: Context) => await runFlow(ctx, { + compile: async () => { + [ctx.executeUser, ctx.executeManager] = await Promise.all([ + ctx.compile(ctx.lang, ctx.code), + ctx.compileLocalFile('manager', ctx.config.manager), + ]); + }, + judgeCase, +}); diff --git a/packages/hydrojudge/src/judge/index.ts b/packages/hydrojudge/src/judge/index.ts index 67e8e8ee3..f8c6103e0 100644 --- a/packages/hydrojudge/src/judge/index.ts +++ b/packages/hydrojudge/src/judge/index.ts @@ -1,3 +1,4 @@ +import * as communication from './communication'; import * as def from './default'; import * as generate from './generate'; import * as hack from './hack'; @@ -8,5 +9,5 @@ import * as run from './run'; import * as submit_answer from './submit_answer'; export = { - default: def, generate, interactive, run, submit_answer, objective, hack, + default: def, generate, interactive, communication, run, submit_answer, objective, hack, } as Record }>; diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 5ec37b58e..8d6fc746e 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -11,7 +11,7 @@ function judgeCase(c: NormalizedCase) { const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang); const [{ code, signalled, time, memory, - }, resInteractor] = await runPiped( + }, resInteractor] = await runPiped([ { execute: ctx.executeUser.execute, copyIn: ctx.executeUser.copyIn, @@ -32,7 +32,10 @@ function judgeCase(c: NormalizedCase) { copyOut: ['/w/tout?'], env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, }, - ); + ], [ + { in: { index: 0, fd: 1 }, out: { index: 1, fd: 0 }, name: 'userToInteractor' }, + { in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 }, name: 'interactorToUser' }, + ]); // TODO handle tout (maybe pass to checker?) let status: number; let score = 0; diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts index 26dd33483..e13ffcd96 100644 --- a/packages/hydrojudge/src/judge/interface.ts +++ b/packages/hydrojudge/src/judge/interface.ts @@ -20,6 +20,7 @@ export interface RuntimeContext { execute?: Execute; checker?: Execute; executeInteractor?: Execute; + executeManager?: Execute; executeUser?: Execute; _callbackAwait?: Promise; diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 9b00b523f..c86cd9d6d 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -9,7 +9,7 @@ import { FormatError, SystemError } from './error'; import { Logger } from './log'; import client from './sandbox/client'; import { - Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus, + Cmd, CopyIn, CopyInFile, PipeMap, SandboxResult, SandboxStatus, } from './sandbox/interface'; import { cmd, parseMemoryMB } from './utils'; @@ -29,7 +29,7 @@ const statusMap: Map = new Map([ [SandboxStatus.Signalled, STATUS.STATUS_RUNTIME_ERROR], ]); -interface Parameter { +export interface Parameter { time?: number; stdin?: CopyInFile; execute?: string; @@ -145,33 +145,24 @@ async function adaptResult(result: SandboxResult, params: Parameter): Promise { +export async function runPiped( + execute: Parameter[], pipeMapping: Pick[], +): Promise { let res: SandboxResult[]; const size = parseMemoryMB(getConfig('stdio_size')); try { const body = { - cmd: [ - proc(execute0), - proc(execute1), - ], - pipeMapping: [{ - in: { index: 0, fd: 1 }, - out: { index: 1, fd: 0 }, + cmd: execute.map((exe) => proc(exe)), + pipeMapping: pipeMapping.map((pipe) => ({ proxy: true, - name: 'stdout', max: 1024 * 1024 * size, - }, { - in: { index: 1, fd: 1 }, - out: { index: 0, fd: 0 }, - proxy: true, - name: 'stdout', - max: 1024 * 1024 * size, - }], + ...pipe, + })), }; - body.cmd[0].files[0] = null; - body.cmd[0].files[1] = null; - body.cmd[1].files[0] = null; - body.cmd[1].files[1] = null; + for (let i = 0; i < body.cmd.length; i++) { + if (pipeMapping.find((pipe) => pipe.out.index === i && pipe.out.fd === 0)) body.cmd[i].files[0] = null; + if (pipeMapping.find((pipe) => pipe.in.index === i && pipe.in.fd === 1)) body.cmd[i].files[1] = null; + } const id = callId++; if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(body)); res = await client.run(body); @@ -181,7 +172,7 @@ export async function runPiped(execute0: Parameter, execute1: Parameter): Promis console.error(e); throw new SystemError('Sandbox Error', [e]); } - return await Promise.all(res.map((r) => adaptResult(r, {}))) as [SandboxAdaptedResult, SandboxAdaptedResult]; + return await Promise.all(res.map((r) => adaptResult(r, {}))) as SandboxAdaptedResult[]; } export async function del(fileId: string) { diff --git a/packages/hydrojudge/src/task.ts b/packages/hydrojudge/src/task.ts index 61d4ddfe1..e8d6847c4 100644 --- a/packages/hydrojudge/src/task.ts +++ b/packages/hydrojudge/src/task.ts @@ -135,7 +135,7 @@ export class JudgeTask { return result; } - async compileLocalFile(type: 'interactor' | 'validator' | 'checker' | 'generator' | 'std', file: string, checkerType?: string) { + async compileLocalFile(type: 'interactor' | 'validator' | 'checker' | 'generator' | 'manager' | 'std', file: string, checkerType?: string) { if (type === 'checker' && ['default', 'strict'].includes(checkerType)) return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) }; if (type === 'checker' && !checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]); const withTestlib = type !== 'std' && (type !== 'checker' || checkerType === 'testlib'); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 354c1da9e..f12c220b6 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -165,6 +165,7 @@ export enum ProblemType { Default = 'default', SubmitAnswer = 'submit_answer', Interactive = 'interactive', + Communication = 'communication', Objective = 'objective', Remote = 'remote_judge', } @@ -196,6 +197,8 @@ export interface ProblemConfigFile { checker_type?: string; checker?: string; interactor?: string; + manager?: string; + num_processes?: number; user_extra_files?: string[]; judge_extra_files?: string[]; detail?: boolean; diff --git a/packages/ui-default/components/monaco/schema/problemconfig.ts b/packages/ui-default/components/monaco/schema/problemconfig.ts index 90682fef2..cc5466de2 100644 --- a/packages/ui-default/components/monaco/schema/problemconfig.ts +++ b/packages/ui-default/components/monaco/schema/problemconfig.ts @@ -52,7 +52,7 @@ const problemConfigSchema: JSONSchema7 = { properties: { redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' }, key: { type: 'string', pattern: '[0-9a-f]{32}' }, - type: { enum: ['default', 'interactive', 'submit_answer', 'objective', 'remote_judge'] }, + type: { enum: ['default', 'interactive', 'communication', 'submit_answer', 'objective', 'remote_judge'] }, subType: { type: 'string' }, langs: { type: 'array', items: { type: 'string' } }, target: { type: 'string' }, @@ -64,6 +64,8 @@ const problemConfigSchema: JSONSchema7 = { ], }, interactor: { type: 'string', pattern: '\\.' }, + manager: { type: 'string', pattern: '\\.' }, + num_processes: { type: 'number', minimum: 1, maximum: 5 }, validator: { type: 'string', pattern: '\\.' }, user_extra_files: { type: 'array', items: { type: 'string' } }, judge_extra_files: { type: 'array', items: { type: 'string' } }, diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index e08e18c2d..1788ad4a6 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -29,9 +29,9 @@ interface Props { const configKey = [ 'type', 'subType', 'target', 'score', 'time', 'memory', 'filename', 'checker_type', 'checker', 'interactor', - 'validator', 'user_extra_files', 'judge_extra_files', 'detail', 'outputs', - 'redirect', 'cases', 'subtasks', 'langs', 'key', - 'time_limit_rate', 'memory_limit_rate', + 'manager', 'num_processes', 'validator', 'user_extra_files', 'judge_extra_files', + 'detail', 'outputs', 'redirect', 'cases', 'subtasks', + 'langs', 'key', 'time_limit_rate', 'memory_limit_rate', ]; const subtasksKey = [ diff --git a/packages/ui-default/components/problemconfig/ProblemType.tsx b/packages/ui-default/components/problemconfig/ProblemType.tsx index 4e9e18126..f830057e2 100644 --- a/packages/ui-default/components/problemconfig/ProblemType.tsx +++ b/packages/ui-default/components/problemconfig/ProblemType.tsx @@ -12,6 +12,7 @@ export default function ProblemType() { const Type = useSelector((state: RootState) => state.config.type); const checkerType = useSelector((state: RootState) => state.config.checker_type); const filename = useSelector((state: RootState) => state.config.filename); + const numProcesses = useSelector((state: RootState) => state.config.num_processes); const subType = useSelector((state: RootState) => state.config.subType); const checker = useSelector((state: RootState) => state.config.checker); const [category, setCategory] = React.useState(''); @@ -133,6 +134,25 @@ export default function ProblemType() { )} /> + + + + + + dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'num_processes', value: +ev.currentTarget.value }))} + className="textbox" + /> + + + )} + /> = {}, check config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.'); } if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); + if (cfg.manager) config.manager = checkFile(cfg.manager, 'Cannot find Manager {0}.'); if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.'); for (const n of ['judge', 'user']) { const conf = cfg[`${n}_extra_files`];