judge: submit_answer

This commit is contained in:
undefined committed 2022-04-03 15:02:32 +08:00
1 parent 5c4221778e
commit 0cb5afa169
18 files changed
+191 -82

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js",
"version": "2.15.5",
"version": "2.16.0",
"main": "package.json",
"author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git",
+1 -3
View File
@@ -1,4 +1,3 @@
import fs from 'fs-extra';
import { findFileSync } from '@hydrooj/utils/lib/utils';
import checkers from './checkers';
import compile from './compile';
@@ -19,7 +18,6 @@ export async function check(config: CheckConfig): Promise<[number, number, strin
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise<Execute> {
if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]);
if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc };
const file = await fs.readFile(checker);
const s = checker.replace('@', '.').split('.');
let lang;
let langId = s.pop();
@@ -30,5 +28,5 @@ export async function compileChecker(getLang: Function, checkerType: string, che
}
if (!lang) throw new SystemError('Unknown checker language.');
// TODO cache compiled checker
return await compile(lang, file.toString(), copyIn);
return await compile(lang, { src: checker }, copyIn);
}
+3 -3
View File
@@ -7,7 +7,7 @@ import { CopyInFile } from './sandbox/interface';
import { compilerText } from './utils';
export = async function compile(
lang: LangConfig, code: string, copyIn: Record<string, CopyInFile> = {}, next?: Function,
lang: LangConfig, code: CopyInFile, copyIn: Record<string, CopyInFile> = {}, next?: Function,
): Promise<Execute> {
const target = lang.target || 'foo';
const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute;
@@ -18,7 +18,7 @@ export = async function compile(
} = await run(
copyIn['compile.sh'] ? '/bin/bash compile.sh' : lang.compile,
{
copyIn: { ...copyIn, [lang.code_file]: { content: code } },
copyIn: { ...copyIn, [lang.code_file]: code },
copyOutCached: [target],
env: { HYDRO_LANG: lang.key },
},
@@ -35,7 +35,7 @@ export = async function compile(
}
return {
execute,
copyIn: { ...copyIn, [lang.code_file]: { content: code } },
copyIn: { ...copyIn, [lang.code_file]: code },
clean: () => Promise.resolve(null),
time,
};
+26 -4
View File
@@ -14,6 +14,7 @@ import { getConfig } from '../config';
import { CompileError, FormatError, SystemError } from '../error';
import judge from '../judge';
import log from '../log';
import { CopyInFile } from '../sandbox/interface';
import * as sysinfo from '../sysinfo';
import * as tmpfs from '../tmpfs';
import {
@@ -31,10 +32,10 @@ class JudgeTask {
source: string;
rid: string;
lang: string;
code: string;
code: CopyInFile;
tmpdir: string;
input?: string;
clean: Function[];
clean: (() => Promise<any>)[];
data: any[];
folder: string;
config: any;
@@ -57,7 +58,7 @@ class JudgeTask {
this.stat.handle = new Date();
this.rid = this.request.rid;
this.lang = this.request.lang;
this.code = this.request.code;
this.code = { content: this.request.code };
this.config = this.request.config;
this.input = this.request.input;
this.data = this.request.data;
@@ -100,7 +101,7 @@ class JudgeTask {
}
} finally {
Lock.release(`${this.host}/${this.source}/${this.rid}`);
for (const clean of this.clean) await clean().catch(noop);
for (const clean of this.clean) await clean()?.catch(noop);
tmpfs.umount(this.tmpdir);
fs.removeSync(this.tmpdir);
}
@@ -109,6 +110,12 @@ class JudgeTask {
async doSubmission(startPromise = Promise.resolve()) {
this.stat.cache_start = new Date();
this.folder = await this.session.cacheOpen(this.source, this.data, this.next);
if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) {
const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0];
const target = await this.session.fetchCodeFile(id);
this.code = { src: target };
this.clean.push(() => fs.remove(target));
}
this.stat.read_cases = new Date();
this.config = await readCases(
this.folder,
@@ -247,6 +254,21 @@ export default class Hydro {
return filePath;
}
async fetchCodeFile(name: string) {
const res = await this.axios.post('judge/code', { id: name });
const f = await this.axios.get(res.data.url, { responseType: 'stream' })
.catch((e) => new Error(`DownloadFail(${name}): ${e.message}`));
if (f instanceof Error) throw f;
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
const w = fs.createWriteStream(target);
f.data.pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
});
return target;
}
getLang(name: string, doThrow = true) {
if (this.language[name]) return this.language[name];
if (name === 'cpp' && this.language.cc) return this.language.cc;
+32 -25
View File
@@ -4,6 +4,7 @@ import { check, compileChecker } from '../check';
import compile from '../compile';
import { getConfig } from '../config';
import { CompileError, FormatError } from '../error';
import { Logger } from '../log';
import { del, run } from '../sandbox';
import signals from '../signals';
import { parseFilename } from '../utils';
@@ -17,6 +18,8 @@ const Score = {
min: Math.min,
};
const logger = new Logger('judge/default');
function judgeCase(c: Case, sid: string) {
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
@@ -91,28 +94,6 @@ function judgeCase(c: Case, sid: string) {
await runner(ctx, ctxSubtask);
return;
}
if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
const langConfig = ctx.getLang(ctx.lang);
if (langConfig.analysis && !ctx.analysis) {
ctx.analysis = true;
run(langConfig.analysis, {
copyIn: {
...copyIn,
input: stdin ? { src: stdin } : { content: '' },
[langConfig.code_file || 'foo']: { content: ctx.code },
compile: { content: langConfig.compile || '' },
execute: { content: langConfig.execute || '' },
},
time: 5000,
memory: 256,
env: ctx.env,
}).then((r) => {
const out = r.stdout.toString();
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
});
}
}
ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, score);
ctxSubtask.status = Math.max(ctxSubtask.status, status);
if (ctxSubtask.status > STATUS.STATUS_ACCEPTED) ctx.failed[sid] = true;
@@ -128,6 +109,32 @@ function judgeCase(c: Case, sid: string) {
},
addProgress: 100 / ctx.config.count,
}, c.id);
if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
const langConfig = ctx.getLang(ctx.lang);
if (langConfig.analysis && !ctx.analysis) {
ctx.analysis = true;
try {
const r = await run(langConfig.analysis, {
copyIn: {
...copyIn,
input: stdin ? { src: stdin } : { content: '' },
[langConfig.code_file || 'foo']: ctx.code,
compile: { content: langConfig.compile || '' },
execute: { content: langConfig.execute || '' },
},
time: 5000,
memory: 256,
env: ctx.env,
});
const out = r.stdout.toString();
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
} catch (e) {
logger.info('Failed to run analysis');
logger.error(e);
}
}
}
};
}
@@ -158,10 +165,10 @@ function judgeSubtask(subtask: SubTask, sid: string) {
export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING }));
if (ctx.config.template) {
if (ctx.config.template && 'content' in ctx.code) {
if (ctx.config.template[ctx.lang]) {
const tpl = ctx.config.template[ctx.lang];
ctx.code = tpl[0] + ctx.code + tpl[1];
ctx.code.content = tpl[0] + ctx.code.content + tpl[1];
} else throw new CompileError('Language not supported by provided templates');
}
[ctx.execute, ctx.checker] = await Promise.all([
@@ -176,7 +183,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
}
const copyIn = { user_code: { content: ctx.code } };
const copyIn = { user_code: ctx.code };
for (const file of ctx.config.judge_extra_files) {
copyIn[parseFilename(file)] = { src: file };
}
+2 -2
View File
@@ -108,14 +108,14 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
(() => {
const copyIn = {
'testlib.h': { src: testlibSrc },
user_code: { content: ctx.code },
user_code: ctx.code,
};
for (const file of ctx.config.judge_extra_files) {
copyIn[parseFilename(file)] = { src: file };
}
return compile(
ctx.getLang(parseFilename(ctx.config.interactor).split('.')[1].replace('@', '.')),
fs.readFileSync(ctx.config.interactor).toString(),
{ src: ctx.config.interactor },
copyIn,
);
})(),
+2 -1
View File
@@ -1,6 +1,7 @@
import PQueue from 'p-queue';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { Execute } from '../interface';
import { CopyInFile } from '../sandbox/interface';
export type Context = JudgeTaskInterface & RuntimeContext;
@@ -29,7 +30,7 @@ export interface JudgeTaskInterface {
stat: Record<string, Date>;
lang: string;
code: string;
code: CopyInFile;
tmpdir: string;
input?: string;
clean: Function[];
+32 -25
View File
@@ -3,6 +3,7 @@ import fs from 'fs-extra';
import { STATUS } from '@hydrooj/utils/lib/status';
import compile from '../compile';
import { CompileError } from '../error';
import { Logger } from '../log';
import { run } from '../sandbox';
import signals from '../signals';
import { compilerText, parseMemoryMB, parseTimeMS } from '../utils';
@@ -16,6 +17,8 @@ const failure = (status: number, message?: string) => ({
message,
});
const logger = new Logger('judge/run');
export const judge = async (ctx: Context) => {
ctx.stat.judge = new Date();
ctx.next({ status: STATUS.STATUS_COMPILING });
@@ -80,31 +83,6 @@ export const judge = async (ctx: Context) => {
if (code < 32) message.push(`ExitCode: ${code} (${signals[code]})`);
else message.push(`ExitCode: ${code}`);
}
if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
const langConfig = ctx.getLang(ctx.lang);
if (langConfig.analysis) {
ctx.analysis = true;
run(langConfig.analysis, {
copyIn: {
...copyIn,
input: { src: stdin },
[langConfig.code_file || 'foo']: { content: ctx.code },
compile: { content: langConfig.compile || '' },
execute: { content: langConfig.execute || '' },
},
env: {
...ctx.env,
HYDRO_PRETEST: 'true',
},
time: 5000,
memory: 256,
}).then((r) => {
const out = r.stdout.toString();
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
});
}
}
message.push(fs.readFileSync(stdout).toString());
message.push(fs.readFileSync(stderr).toString());
ctx.next({
@@ -116,6 +94,35 @@ export const judge = async (ctx: Context) => {
message: message.join('\n').substring(0, 102400),
},
});
if ([STATUS.STATUS_WRONG_ANSWER, STATUS.STATUS_RUNTIME_ERROR].includes(status)) {
const langConfig = ctx.getLang(ctx.lang);
if (langConfig.analysis) {
try {
ctx.analysis = true;
const r = await run(langConfig.analysis, {
copyIn: {
...copyIn,
input: { src: stdin },
[langConfig.code_file || 'foo']: ctx.code,
compile: { content: langConfig.compile || '' },
execute: { content: langConfig.execute || '' },
},
env: {
...ctx.env,
HYDRO_PRETEST: 'true',
},
time: 5000,
memory: 256,
});
const out = r.stdout.toString();
if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
} catch (e) {
logger.info('Failed to run analysis');
logger.error(e);
}
}
}
ctx.stat.done = new Date();
if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) });
ctx.end({
@@ -1,12 +1,19 @@
import { readFile } from 'fs-extra';
import { STATUS } from '@hydrooj/utils/lib/status';
import { Context } from './interface';
// TODO drop this format in next major version
// NOT RECOMMENDED TO USE
export async function judge({
next, end, config, code,
}: Context) {
next({ status: STATUS.STATUS_JUDGING, progress: 0 });
code = code.replace(/\r/g, '');
const outputs = code.split('\n');
const answer = ('src' in code)
? await readFile(code.src, 'utf-8')
: ('content' in code)
? code.content.toString().replace(/\r/g, '')
: '';
const outputs = answer.split('\n');
let totalScore = 0;
let totalStatus = 0;
for (const i in config.outputs) {
+25 -3
View File
@@ -12,6 +12,7 @@ import * as monitor from 'hydrooj/src/service/monitor';
import readCases from './cases';
import { getConfig } from './config';
import { CompileError, FormatError, SystemError } from './error';
import { CopyInFile } from './sandbox/interface';
import * as sysinfo from './sysinfo';
import * as tmpfs from './tmpfs';
import { compilerText, md5 } from './utils';
@@ -74,6 +75,21 @@ async function postInit() {
}
}
async function fetchCodeFile(name: string) {
const res = await this.axios.post('judge/code', { id: name });
const f = await this.axios.get(res.body.link, { responseType: 'stream' })
.catch((e) => new Error(`DownloadFail(${name}): ${e.message}`));
if (f instanceof Error) throw f;
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
const w = fs.createWriteStream(target);
f.data.pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
});
return target;
}
async function cacheOpen(source: string, files: any[]) {
const filePath = path.join(getConfig('cache_dir'), source);
await fs.ensureDir(filePath);
@@ -148,7 +164,7 @@ async function postInit() {
source: string;
rid: string;
lang: string;
code: string;
code: CopyInFile;
data: any[];
config: any;
env: any;
@@ -156,7 +172,7 @@ async function postInit() {
next: (data: any, id?: number) => void;
end: (data: any) => void;
tmpdir: string;
clean: any[];
clean: (() => Promise<any>)[];
folder: string;
getLang: (lang: string) => any;
@@ -173,7 +189,7 @@ async function postInit() {
this.stat.handle = new Date();
this.rid = this.request.rid.toString();
this.lang = this.request.lang;
this.code = this.request.code;
this.code = { content: this.request.code };
this.data = this.request.data;
this.source = this.request.source;
this.config = this.request.config;
@@ -223,6 +239,12 @@ async function postInit() {
async submission() {
this.stat.cache_start = new Date();
this.folder = await cacheOpen(this.source, this.data);
if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) {
const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0];
const target = await fetchCodeFile(id);
this.code = { src: target };
this.clean.push(() => fs.remove(target));
}
this.stat.read_cases = new Date();
this.config = await readCases(
this.folder,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "3.11.4",
"version": "3.11.5",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
+1
View File
@@ -12,6 +12,7 @@ homepage:
- width: 3
hitokoto: true
starred_problems: 50
problem_search: true
discussion_nodes: true
suggestion: true
langs:
+9 -1
View File
@@ -117,13 +117,20 @@ export class JudgeFilesDownloadHandler extends Handler {
// eslint-disable-next-line no-await-in-loop
links[file] = await storage.signDownloadLink(
`problem/${pdoc.domainId}/${pdoc.docId}/testdata/${file}`,
file, false, 'judge',
file, true, 'judge',
);
}
this.response.body.links = links;
}
}
export class SubmissionDataDownloadHandler extends Handler {
@post('id', Types.String)
async post(domainId: string, id: string) {
this.response.body = { url: await storage.signDownloadLink(`submission/${id}`, 'code', true, 'judge') };
}
}
class JudgeConnectionHandler extends ConnectionHandler {
processing: any = null;
closed = false;
@@ -190,6 +197,7 @@ class JudgeConnectionHandler extends ConnectionHandler {
export async function apply() {
Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE);
Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE);
Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
}
+16 -5
View File
@@ -2,6 +2,7 @@ import AdmZip from 'adm-zip';
import { statSync } from 'fs-extra';
import { intersection, isSafeInteger } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
import { sortFiles } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
@@ -462,7 +463,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
}
@param('lang', Types.Name)
@param('code', Types.Content)
@param('code', Types.Content, true)
@param('pretest', Types.Boolean)
@param('input', Types.String, true)
@param('tid', Types.ObjectID, true)
@@ -470,11 +471,21 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
if (this.response.body.pdoc.config?.langs && !this.response.body.pdoc.config.langs.includes(lang)) {
throw new BadRequestError('Language not allowed.');
}
if (pretest && setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest;
if (pretest && setting.langs[lang]?.pretest === false) throw new BadRequestError('Cannot run pretest for this language.');
if (pretest && !['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
throw new BadRequestError('unable to run pretest');
if (pretest) {
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest;
if (setting.langs[lang]?.pretest === false) throw new BadRequestError('Cannot run pretest for this language.');
if (!['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
throw new BadRequestError('unable to run pretest');
}
}
if (!code) {
const file = this.request.files?.file;
if (!file) throw new ValidationError('code');
if (file.size > 128 * 1024 * 1024) throw new ValidationError('file');
const id = nanoid();
await storage.put(`submission/${this.user._id}/${id}`, file.path);
code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`;
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
await this.limitRate('add_record', 60, system.get('limit.submission'));
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, pretest ? input : tid, tid && !pretest);
const rdoc = await record.get(domainId, rid);
+15 -1
View File
@@ -8,6 +8,8 @@ import { PERM, PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
import problem from '../model/problem';
import record from '../model/record';
import { langs } from '../model/setting';
import storage from '../model/storage';
import * as system from '../model/system';
import TaskModel from '../model/task';
import user from '../model/user';
@@ -101,7 +103,8 @@ class RecordListHandler extends Handler {
class RecordDetailHandler extends Handler {
@param('rid', Types.ObjectID)
async get(domainId: string, rid: ObjectID) {
@param('download', Types.Boolean)
async get(domainId: string, rid: ObjectID, download = false) {
this.response.template = 'record_detail.html';
const rdoc = await record.get(domainId, rid);
if (!rdoc) throw new RecordNotFoundError(rid);
@@ -132,6 +135,17 @@ class RecordDetailHandler extends Handler {
rdoc.compilerTexts = [];
}
if (download && rdoc.code.startsWith('@@hydro_submission_file@@')) {
const [id, filename] = rdoc.code.split('@@hydro_submission_file@@')[1].split('#');
this.response.redirect = await storage.signDownloadLink(`submission/${id}`, filename || 'code', true, 'judge');
return;
}
if (download) {
const lang = langs[rdoc.lang]?.pretest || rdoc.lang;
this.response.body = rdoc.code;
this.response.disposition = `attachment; filename="${langs[lang].code_file}` || `foo.${rdoc.lang}"`;
return;
}
if (pdoc && !(rdoc.contest && this.user._id === rdoc.uid)) {
if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.34.31",
"version": "4.34.32",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -8,7 +8,7 @@
<h1 class="section__title">{{ _('Submit to Judge') }}</h1>
</div>
<div class="section__body">
<form method="post">
<form method="post" enctype="multipart/form-data">
<div class="hasjs--hide">
{{ form.form_select({
label:'Code language',
@@ -36,6 +36,12 @@
</div>
</div>
{{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }}
<div class="row">
<div class="medium-12 columns">
<p>{{ _('Or upload a file:') }} <input type="file" name="file"></p>
<br />
</div>
</div>
<div class="row"><div class="columns">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
@@ -9,10 +9,15 @@
<div class="section">
<div class="section__header">
<h1 class="section__title">{{ _('Code') }}</h1>
<div class="section__tools">
<a class="primary rounded button" href="?download=true"><span class="icon icon-download"></span> {{ _('Download') }}</a>
</div>
</div>
<div class="section__body">
<pre><code class="line-numbers language-{{ model.setting.langs[rdoc.lang].highlight }}">{{ rdoc['code'] }}</code></pre>
</div>
{% if not rdoc.code.startsWith('@@hydro_submission_file@@') %}
<div class="section__body">
<pre><code class="line-numbers language-{{ model.setting.langs[rdoc.lang].highlight }}">{{ rdoc['code'] }}</code></pre>
</div>
{% endif %}
</div>
{% endif %}
</div>