Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/hydrojudge/src/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function isValidConfig(config) {
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.');
Expand Down
2 changes: 1 addition & 1 deletion packages/hydrojudge/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('.');
Expand Down
91 changes: 91 additions & 0 deletions packages/hydrojudge/src/judge/communication.ts
Original file line number Diff line number Diff line change
@@ -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,
});
3 changes: 2 additions & 1 deletion packages/hydrojudge/src/judge/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as communication from './communication';
import * as def from './default';
import * as generate from './generate';
import * as hack from './hack';
Expand All @@ -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> }>;
7 changes: 5 additions & 2 deletions packages/hydrojudge/src/judge/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 } },
{ in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 } },
]);
// TODO handle tout (maybe pass to checker?)
let status: number;
let score = 0;
Expand Down
1 change: 1 addition & 0 deletions packages/hydrojudge/src/judge/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface RuntimeContext {
execute?: Execute;
checker?: Execute;
executeInteractor?: Execute;
executeManager?: Execute;
executeUser?: Execute;

_callbackAwait?: Promise<any>;
Expand Down
37 changes: 15 additions & 22 deletions packages/hydrojudge/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { gte } from 'semver';
import { ParseEntry } from 'shell-quote';
import { STATUS } from '@hydrooj/utils/lib/status';
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
import { Optional } from 'hydrooj';
import { getConfig } from './config';
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';

Expand All @@ -29,7 +30,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;
Expand Down Expand Up @@ -145,33 +146,25 @@ 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: Optional<PipeMap, 'proxy' | 'name' | 'max'>[],
): 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);
Expand All @@ -181,7 +174,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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/hydrojudge/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
3 changes: 3 additions & 0 deletions packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export enum ProblemType {
Default = 'default',
SubmitAnswer = 'submit_answer',
Interactive = 'interactive',
Communication = 'communication',
Objective = 'objective',
Remote = 'remote_judge',
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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' } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
20 changes: 20 additions & 0 deletions packages/ui-default/components/problemconfig/ProblemType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -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')}
Expand Down
1 change: 1 addition & 0 deletions packages/ui-default/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/ui-default/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 提交答案题
Expand Down
1 change: 1 addition & 0 deletions packages/utils/lib/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`];
Expand Down
Loading