mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
judge: submit_answer
This commit is contained in:
1 parent
5c4221778e
commit
0cb5afa169
18 files changed
+191
-82
No files matched your search
@@ -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,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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
})(),
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.11.4",
|
||||
"version": "3.11.5",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -12,6 +12,7 @@ homepage:
|
||||
- width: 3
|
||||
hitokoto: true
|
||||
starred_problems: 50
|
||||
problem_search: true
|
||||
discussion_nodes: true
|
||||
suggestion: true
|
||||
langs:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in new issue
Block a user