judge: add communication problem support (#834) (#925)

Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
Milmonandundefined authored and GitHub committed 2025-01-21 07:33:39 +08:00
1 parent abf1c7205e
commit ebc586daba
15 files changed
+151 -33

No files matched your search

+4 -1
View File
@@ -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.');
+1 -1
View File
@@ -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('.');
@@ -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,
});
+2 -1
View File
@@ -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<string, { judge(ctx: Context): Promise<void> }>;
+5 -2
View File
@@ -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;
@@ -20,6 +20,7 @@ export interface RuntimeContext {
execute?: Execute;
checker?: Execute;
executeInteractor?: Execute;
executeManager?: Execute;
executeUser?: Execute;
_callbackAwait?: Promise<any>;
+14 -23
View File
@@ -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<SandboxStatus, number> = 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<Sa
return ret;
}
export async function runPiped(execute0: Parameter, execute1: Parameter): Promise<[SandboxAdaptedResult, SandboxAdaptedResult]> {
export async function runPiped(
execute: Parameter[], pipeMapping: Pick<PipeMap, 'in' | 'out' | 'name'>[],
): Promise<SandboxAdaptedResult[]> {
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) {
+1 -1
View File
@@ -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');
+3
View File
@@ -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;
@@ -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' } },
@@ -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 = [
@@ -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() {
</div>
)}
/>
<Tab
id="communication"
title={i18n('problem_type.communication')}
panel={(
<div className="row">
<FormItem columns={6} label="Manager">
<SingleFileSelect formKey="manager" />
</FormItem>
<FormItem columns={6} label="Number of Processes">
<input
defaultValue={numProcesses || 2}
placeholder="2"
onChange={(ev) => dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'num_processes', value: +ev.currentTarget.value }))}
className="textbox"
/>
</FormItem>
</div>
)}
/>
<Tab
id="submit_answer"
title={i18n('problem_type.submit_answer')}
+1
View File
@@ -29,6 +29,7 @@ problem_solution: Problem Solution
problem_submit: Problem Submit
problem_type.default: 'Type: Default'
problem_type.interactive: 'Type: Interactive'
problem_type.communication: 'Type: Communication'
problem_type.objective: 'Type: Objective'
problem_type.remote_judge: 'Type: RemoteJudge'
problem_type.submit_answer: 'Type: SubmitAnswer'
+1
View File
@@ -700,6 +700,7 @@ problem_solution: 题解
problem_submit: 递交代码
problem_type.default: 传统题
problem_type.interactive: 交互题
problem_type.communication: 通信题
problem_type.objective: 客观题
problem_type.remote_judge: 远端评测题
problem_type.submit_answer: 提交答案题
+1
View File
@@ -19,6 +19,7 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, 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`];