judge: bug fix

This commit is contained in:
undefined committed 2022-09-06 13:02:46 +08:00
1 parent 511d08c850
commit 07ab4f1bbd
12 files changed
+111 -79

No files matched your search

+1 -1
View File
@@ -7,7 +7,6 @@
"repository": "https://github.com/hydro-dev/Hydro.git",
"dependencies": {
"@hydrooj/utils": "workspace:*",
"axios": "^0.27.2",
"cac": "^6.7.14",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
@@ -15,6 +14,7 @@
"p-queue": "^7.3.0",
"schemastery": "^3.5.3",
"shell-quote": "^1.7.3",
"superagent": "^8.0.0",
"ws": "^8.8.1"
},
"preferUnplugged": true,
+17 -22
View File
@@ -4,11 +4,13 @@ import { findFileSync } from '@hydrooj/utils/lib/utils';
import checkers from './checkers';
import { CompileError, FormatError } from './error';
import { Execute } from './interface';
import { CopyInFile, del, run } from './sandbox';
import {
CopyIn, CopyInFile, del, run,
} from './sandbox';
import { compilerText } from './utils';
export default async function compile(
lang: LangConfig, code: CopyInFile, copyIn: Record<string, CopyInFile> = {}, next?: Function,
lang: LangConfig, code: CopyInFile, copyIn: CopyIn = {}, next?: Function,
): Promise<Execute> {
const target = lang.target || 'foo';
const execute = copyIn['execute.sh'] ? '/bin/bash execute.sh' : lang.execute;
@@ -46,8 +48,8 @@ const testlibFile = {
src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'),
};
const guessLanguage = (filename: string, getLang: Function) => {
const s = filename.replace('@', '.').split('.');
async function _compile(src: string, type: 'checker' | 'validator' | 'interactor', getLang, copyIn, withTestlib = true) {
const s = src.replace('@', '.').split('.');
let lang;
let langId = s.pop();
while (s.length) {
@@ -55,31 +57,24 @@ const guessLanguage = (filename: string, getLang: Function) => {
if (lang) break;
langId = `${s.pop()}.${langId}`;
}
return lang;
};
if (!lang) throw new FormatError(`Unknown ${type} language.`);
if (withTestlib) copyIn = { ...copyIn, 'testlib.h': testlibFile };
// TODO cache compiled binary
return await compile(lang, { src }, copyIn);
}
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: CopyIn) {
if (['default', 'strict'].includes(checkerType)) {
return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
}
if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
if (checkerType === 'testlib') copyIn['testlib.h'] = testlibFile;
const lang = guessLanguage(checker, getLang);
if (!lang) throw new FormatError('Unknown checker language.');
// TODO cache compiled checker
return await compile(lang, { src: checker }, copyIn);
return _compile(checker, 'checker', getLang, copyIn, checkerType === 'testlib');
}
export async function compileInteractor(getLang: Function, interactor: string, copyIn: any): Promise<Execute> {
const lang = guessLanguage(interactor, getLang);
if (!lang) throw new FormatError('Unknown interactor language.');
// TODO cache compiled checker
return await compile(lang, { src: interactor }, { ...copyIn, 'testlib.h': testlibFile });
export async function compileInteractor(getLang: Function, interactor: string, copyIn: CopyIn) {
return _compile(interactor, 'interactor', getLang, copyIn);
}
export async function compileValidator(getLang: Function, validator: string, copyIn: any): Promise<Execute> {
const lang = guessLanguage(validator, getLang);
if (!lang) throw new FormatError('Unknown validator language.');
// TODO cache compiled checker
return await compile(lang, { src: validator }, { ...copyIn, 'testlib.h': testlibFile });
export async function compileValidator(getLang: Function, validator: string, copyIn: CopyIn) {
return _compile(validator, 'validator', getLang, copyIn);
}
+4 -2
View File
@@ -20,11 +20,13 @@ const JudgeSettings = Schema.object({
processLimit: Schema.number().default(32),
parallelism: Schema.number().default(2),
singleTaskParallelism: Schema.number().default(2),
rerun: Schema.number().default(0),
rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0),
rate: Schema.number().default(1),
env: Schema.string().default('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nHOME=/w'),
host: Schema.any(),
secret: Schema.string().default(String.random(32)),
secret: Schema.string().description('Judge Token Secret').default(String.random(32)),
disable: Schema.boolean().description('Disable builtin judge').default(false),
detail: Schema.boolean().description('Show diff detail').default(true),
});
const config = global.Hydro
+2 -2
View File
@@ -6,9 +6,9 @@ import type { JudgeResultBody } from 'hydrooj';
import { end, next } from 'hydrooj/src/handler/judge';
import { Logger } from 'hydrooj/src/logger';
import * as setting from 'hydrooj/src/model/setting';
import storage from 'hydrooj/src/model/storage';
import * as system from 'hydrooj/src/model/system';
import task from 'hydrooj/src/model/task';
import storage from 'hydrooj/src/service/storage';
import { processTestdata } from '../cases';
import { getConfig } from '../config';
import { FormatError, SystemError } from '../error';
@@ -26,7 +26,7 @@ const session = {
},
getNext(t: Context) {
return (data: Partial<JudgeResultBody>) => {
logger.debug('Next: %d %o', data);
logger.debug('Next: %o', data);
data.rid = t.rid as any;
if (data.case) data.case.message ||= '';
next(data);
+22 -30
View File
@@ -1,9 +1,9 @@
/* eslint-disable no-await-in-loop */
import path from 'path';
import axios from 'axios';
import { ObjectID } from 'bson';
import fs from 'fs-extra';
import PQueue from 'p-queue';
import superagent from 'superagent';
import WebSocket from 'ws';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
@@ -20,13 +20,10 @@ function removeNixPath(text: string) {
}
export default class Hydro {
config: any;
axios: any;
ws: WebSocket;
language: Record<string, LangConfig>;
constructor(config) {
this.config = config;
constructor(public config) {
this.config.detail = this.config.detail ?? true;
this.config.cookie = this.config.cookie || '';
this.config.last_update_at = this.config.last_update_at || 0;
@@ -35,10 +32,20 @@ export default class Hydro {
this.getLang = this.getLang.bind(this);
}
get(url) {
return superagent.get(this.config.server_url + url).set('Cookie', this.config.cookie);
}
post(url, data) {
return superagent.post(this.config.server_url + url).send(data)
.set('Cookie', this.config.cookie)
.set('Accept', 'application/json');
}
async init() {
await this.setCookie(this.config.cookie || '');
await this.ensureLogin();
setInterval(() => { this.axios.get(''); }, 30000000); // Cookie refresh only
setInterval(() => { this.get(''); }, 30000000); // Cookie refresh only
}
async cacheOpen(source: string, files: any[], next?) {
@@ -74,18 +81,15 @@ export default class Hydro {
log.info(`Getting problem data: ${this.config.host}/${source}`);
if (next) next({ message: 'Syncing testdata, please wait...' });
await this.ensureLogin();
const res = await this.axios.post(`/d/${domainId}/judge/files`, {
const res = await this.post(`/d/${domainId}/judge/files`, {
pid: +pid,
files: filenames,
});
// eslint-disable-next-line no-inner-declarations
async function download(name: string) {
if (name.includes('/')) await fs.ensureDir(path.join(filePath, name.split('/')[0]));
const f = await this.axios.get(res.data.links[name], { responseType: 'stream' })
.catch((e) => new Error(`DownloadFail(${name}): ${e.message}`));
if (f instanceof Error) throw f;
const w = fs.createWriteStream(path.join(filePath, name));
f.data.pipe(w);
this.get(res.body.links[name]).pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
@@ -93,7 +97,7 @@ export default class Hydro {
}
const tasks = [];
const queue = new PQueue({ concurrency: 10 });
for (const name in res.data.links) {
for (const name in res.body.links) {
tasks.push(queue.add(() => download.call(this, name)));
}
queue.start();
@@ -106,13 +110,10 @@ export default class Hydro {
}
async fetchFile(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 res = await this.post('judge/code', { id: name });
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
const w = fs.createWriteStream(target);
f.data.pipe(w);
this.get(res.body.url).pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', (e) => reject(new Error(`DownloadFail(${name}): ${e.message}`)));
@@ -127,7 +128,7 @@ export default class Hydro {
return null;
}
send(rid: string, key: 'next' | 'end', data: Partial<JudgeResultBody>) {
send(rid: string | ObjectID, key: 'next' | 'end', data: Partial<JudgeResultBody>) {
data.rid = new ObjectID(rid);
data.key = key;
if (data.case) data.case.message ||= '';
@@ -193,20 +194,11 @@ export default class Hydro {
async setCookie(cookie: string) {
this.config.cookie = cookie;
this.axios = axios.create({
baseURL: this.config.server_url,
timeout: 30000,
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
cookie: this.config.cookie,
},
});
}
async login() {
log.info('[%s] Updating session', this.config.host);
const res = await this.axios.post('login', {
const res = await this.post('login', {
uname: this.config.uname, password: this.config.password, rememberme: 'on',
});
await this.setCookie(res.headers['set-cookie'].join(';'));
@@ -214,9 +206,9 @@ export default class Hydro {
async ensureLogin() {
try {
const res = await this.axios.get('judge/files');
const res = await this.get('judge/files');
// Redirected to /login
if (res.data.url) await this.login();
if (res.body.url) await this.login();
} catch (e) {
await this.login();
}
+6 -4
View File
@@ -8,7 +8,7 @@ import { FormatError, SystemError } from './error';
import { Logger } from './log';
import { SandboxClient } from './sandbox/client';
import {
Cmd, CopyInFile, SandboxResult, SandboxStatus,
Cmd, CopyIn, CopyInFile, SandboxResult, SandboxStatus,
} from './sandbox/interface';
import { cmd, parseMemoryMB } from './utils';
@@ -36,7 +36,7 @@ interface Parameter {
execute?: string;
memory?: number;
processLimit?: number;
copyIn?: Record<string, CopyInFile>;
copyIn?: CopyIn;
copyOut?: string[];
copyOutCached?: string[];
cacheStdoutAndStderr?: boolean;
@@ -162,7 +162,8 @@ export async function runPiped(execute0: Parameter, execute1: Parameter): Promis
res = await new SandboxClient(getConfig('sandbox_host')).run(body);
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res));
} catch (e) {
if (e instanceof FormatError) throw e;
if (e instanceof FormatError || e instanceof SystemError) throw e;
console.error(e);
throw new SystemError('Sandbox Error', [e]);
}
return await Promise.all(res.map((r) => adaptResult(r, {}))) as [SandboxAdaptedResult, SandboxAdaptedResult];
@@ -188,7 +189,8 @@ export async function run(execute: string, params?: Parameter): Promise<SandboxA
if (argv.options.showSandbox) logger.debug('%d %s', id, JSON.stringify(res));
[result] = res;
} catch (e) {
if (e instanceof FormatError) throw e;
if (e instanceof FormatError || e instanceof SystemError) throw e;
console.error(e);
// FIXME request body larger than maxBodyLength limit
throw new SystemError('Sandbox Error', e.message);
}
+9 -11
View File
@@ -1,30 +1,28 @@
import axios, { AxiosInstance } from 'axios';
import superagent from 'superagent';
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
export class SandboxClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({ baseURL });
constructor(public baseURL: string) {
if (baseURL.endsWith('/')) baseURL = baseURL.substring(0, baseURL.length - 1);
}
public run(req: SandboxRequest): Promise<SandboxResult[]> {
return this.client.post('/run', req).then((res) => res.data);
return superagent.post(`${this.baseURL}/run`).send(req).then((res) => res.body);
}
public getFile(fileId: string): Promise<Buffer> {
return this.client.get(`/file/${fileId}`).then((res) => res.data);
return superagent.get(`${this.baseURL}/file/${fileId}`).then((res) => res.body);
}
public deleteFile(fileId: string): Promise<never> {
return this.client.delete(`/file/${fileId}`);
public deleteFile(fileId: string): Promise<void> {
return superagent.delete(`${this.baseURL}/file/${fileId}`).then((res) => res.body);
}
public listFiles(): Promise<Record<string, string>> {
return this.client.get('/file').then((res) => res.data);
return superagent.get(`${this.baseURL}/file`).then((res) => res.body);
}
public version(): Promise<SandboxVersion> {
return this.client.get('/version').then((res) => res.data);
return superagent.get(`${this.baseURL}/version`).then((res) => res.body);
}
}
@@ -26,6 +26,7 @@ export interface Collector {
}
export type CopyInFile = LocalFile | MemoryFile | PreparedFile;
export type CopyIn = Record<string, CopyInFile>;
export type CmdFile = LocalFile | MemoryFile | PreparedFile | Collector | null;
/**
+34 -6
View File
@@ -3,7 +3,9 @@ import fs from 'fs-extra';
import { noop } from 'lodash';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { STATUS } from '@hydrooj/utils/lib/status';
import type { JudgeResultBody } from 'hydrooj';
import type {
FileInfo, JudgeRequest, JudgeResultBody, ProblemConfigFile,
} from 'hydrooj';
import { Logger } from 'hydrooj/src/logger';
import readCases from './cases';
import { getConfig } from './config';
@@ -34,14 +36,14 @@ export class JudgeTask {
tmpdir: string;
input?: string;
clean: (() => Promise<any>)[];
data: any[];
data: FileInfo[];
folder: string;
config: any;
config: ProblemConfigFile & { problemOwner: number };
next: (data: Partial<JudgeResultBody>) => void;
end: (data: Partial<JudgeResultBody>) => void;
env: Record<string, string>;
constructor(public session: Session, public request) {
constructor(public session: Session, public request: JudgeRequest) {
this.stat = {};
this.stat.receive = new Date();
logger.debug('%o', request);
@@ -75,7 +77,9 @@ export class JudgeTask {
fs.ensureDirSync(this.tmpdir);
tmpfs.mount(this.tmpdir, getConfig('tmpfs_size'));
logger.info('Submission: %s/%s/%s', host, this.source, this.rid);
await this.doSubmission();
this.stat.cache_start = new Date();
if (tid === '000000000000000000000001') await this.doHack();
else await this.doSubmission();
} catch (e) {
if (e instanceof CompileError) {
this.next({ compilerText: compilerText(e.stdout, e.stderr) });
@@ -105,7 +109,6 @@ export class JudgeTask {
}
async doSubmission() {
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];
@@ -129,4 +132,29 @@ export class JudgeTask {
if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]);
await judge[type].judge(this);
}
async doHack() {
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];
if (!id) throw new SystemError('Submission File Not Found');
const target = await this.session.fetchFile(id);
this.code = { src: target };
this.clean.push(() => fs.remove(target));
}
this.stat.read_cases = new Date();
this.config = await readCases(
this.folder,
{
detail: this.session.config.detail,
isSelfSubmission: this.config.problemOwner === this.request.uid,
...this.config,
},
{ next: this.next, key: md5(`${this.source}/${getConfig('secret')}`) },
);
this.stat.judge = new Date();
const type = typeof this.input === 'string' ? 'run' : this.config.type || 'default';
if (type !== 'default') throw new FormatError('Cannot hack: only type=default allowed');
await judge.hack.judge(this);
}
}
+10
View File
@@ -167,6 +167,7 @@ export interface ProblemConfigFile {
cases?: TestCaseConfig[];
subtasks?: SubtaskConfig[];
langs?: string[];
validator?: string;
}
export interface ProblemConfig {
@@ -298,6 +299,15 @@ export interface RecordDoc {
contest?: ObjectID;
}
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
priority: number;
type: 'judge';
rid: ObjectID;
config: ProblemConfigFile & { problemOwner: number };
data: FileInfo[];
source: string;
}
export interface ScoreboardNode {
type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score';
value: string;
@@ -49,6 +49,7 @@ const problemConfigSchema = {
checker_type: { enum: ['default', 'lemon', 'syzoj', 'hustoj', 'testlib', 'strict', 'qduoj'] },
checker: { type: 'string', pattern: '\\.' },
interactor: { type: 'string', pattern: '\\.' },
validator: { type: 'string', pattern: '\\.' },
user_extra_files: { type: 'array', items: { type: 'string' } },
judge_extra_files: { type: 'array', items: { type: 'string' } },
cases: { $ref: '#/def/cases' },
+4 -1
View File
@@ -8,7 +8,9 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
user_extra_files: [],
};
if (cfg.type === 'objective') {
if (cfg.checker || cfg.interactor) throw new Error('You cannot use checker or interactor for objective questions');
if (cfg.checker || cfg.interactor || cfg.validator) {
throw new Error('You cannot use checker, interactor or validator for objective questions');
}
} else {
if (cfg.checker) {
if (!cfg.checker.includes('.')) {
@@ -17,6 +19,7 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.');
}
if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.');
if (cfg.validator) config.validator = checkFile(cfg.validator, 'Cannot find validator {0}.');
['judge', 'user'].forEach((n) => {
const conf = cfg[`${n}_extra_files`];
if (!conf) return;