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