mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
feats::
统一语言配置位置 支持langs继承 支持归类
This commit is contained in:
1 parent
5902b4d4b6
commit
377ef55d49
27 files changed
+252
-657
No files matched your search
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "@hydrooj/hydrojudge",
|
||||
"bin": "bin/hydrojudge.js",
|
||||
"version": "2.5.21",
|
||||
"version": "2.6.0",
|
||||
"main": "package.json",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
"dependencies": {
|
||||
"@hydrooj/utils": "^1.0.9",
|
||||
"adm-zip": "^0.5.5",
|
||||
"@hydrooj/utils": "^1.0.14",
|
||||
"axios": "^0.21.1",
|
||||
"bson": "^4.3.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
|
||||
@@ -52,101 +52,3 @@ disable:
|
||||
name: disable
|
||||
desc: Disable builtin judge
|
||||
default: false
|
||||
langs:
|
||||
type: textarea
|
||||
name: langs
|
||||
desc: Language file settings
|
||||
default: |
|
||||
ccWithoutO2: # Do not remove this entry
|
||||
type: compiler
|
||||
compile: /usr/bin/g++ -Wall -o ${name} foo.cc
|
||||
code_file: foo.cc
|
||||
execute: ${dir}/${name}
|
||||
c:
|
||||
type: compiler
|
||||
compile: /usr/bin/gcc -O2 -Wall -std=c99 -o ${name} foo.c -lm
|
||||
code_file: foo.c
|
||||
execute: ${dir}/${name}
|
||||
cc:
|
||||
type: compiler
|
||||
compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm
|
||||
code_file: foo.cc
|
||||
execute: ${dir}/${name}
|
||||
cc98:
|
||||
type: compiler
|
||||
compile: /usr/bin/g++ -O2 -Wall -std=c++98 -o ${name} foo.cc -lm
|
||||
code_file: foo.cc
|
||||
execute: ${dir}/${name}
|
||||
cc11:
|
||||
type: compiler
|
||||
compile: /usr/bin/g++ -O2 -Wall -std=c++11 -o ${name} foo.cc -lm
|
||||
code_file: foo.cc
|
||||
execute: ${dir}/${name}
|
||||
cc17:
|
||||
type: compiler
|
||||
compile: /usr/bin/g++ -O2 -Wall -std=c++17 -o ${name} foo.cc -lm
|
||||
code_file: foo.cc
|
||||
execute: ${dir}/${name}
|
||||
pas:
|
||||
type: compiler
|
||||
compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas
|
||||
code_file: foo.pas
|
||||
execute: ${dir}/${name}
|
||||
java:
|
||||
type: compiler
|
||||
compile: /usr/bin/javac -d ${dir} -encoding utf8 ./Main.java
|
||||
code_file: Main.java
|
||||
target: Main.class
|
||||
execute: /usr/bin/java Main
|
||||
time_limit_rate: 2
|
||||
py:
|
||||
type: compiler
|
||||
compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)"
|
||||
code_file: foo.py
|
||||
execute: /usr/bin/python ${name}
|
||||
py2:
|
||||
type: compiler
|
||||
compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)"
|
||||
code_file: foo.py
|
||||
execute: /usr/bin/python ${name}
|
||||
py3:
|
||||
type: compiler
|
||||
compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)"
|
||||
code_file: foo.py
|
||||
execute: /usr/bin/python3 ${name}
|
||||
php:
|
||||
type: interpreter
|
||||
code_file: foo.php
|
||||
execute: /usr/bin/php foo.php
|
||||
rs:
|
||||
type: compiler
|
||||
compile: /usr/bin/rustc -O -o ${dir}/${name} ${dir}/foo.rs
|
||||
code_file: foo.rs
|
||||
execute: ${dir}/${name}
|
||||
hs:
|
||||
type: compiler
|
||||
compile: /usr/bin/ghc -O -outputdir /tmp -o ${name} foo.hs
|
||||
code_file: foo.hs
|
||||
execute: ${dir}/${name}
|
||||
js:
|
||||
type: interpreter
|
||||
code_file: foo.js
|
||||
execute: /usr/bin/jsc ${dir}/foo.js
|
||||
go:
|
||||
type: compiler
|
||||
compile: /usr/bin/go build -o ${name} foo.go
|
||||
code_file: foo.go
|
||||
execute: ${dir}/${name}
|
||||
rb:
|
||||
type: interpreter
|
||||
code_file: foo.rb
|
||||
execute: /usr/bin/ruby foo.rb
|
||||
cs:
|
||||
type: compiler
|
||||
compile: /usr/bin/mcs -optimize+ -out:${dir}/${name} ${dir}/foo.cs
|
||||
code_file: foo.cs
|
||||
execute: /usr/bin/mono ${name}
|
||||
jl:
|
||||
type: interpreter
|
||||
code_file: foo.jl
|
||||
execute: /usr/bin/julia ${dir}/foo.jl
|
||||
@@ -24,10 +24,10 @@ export async function check(config): Promise<[number, number, string]> {
|
||||
return [status, score, message];
|
||||
}
|
||||
|
||||
export async function compileChecker(checkerType: string, checker: string, copyIn: any) {
|
||||
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any) {
|
||||
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);
|
||||
// TODO cache compiled checker
|
||||
return await compile(parseFilename(checker).split('.')[1], file.toString(), 'checker', copyIn);
|
||||
return await compile(getLang(parseFilename(checker).split('.')[1]), file.toString(), 'checker', copyIn);
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
import yaml from 'js-yaml';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import * as STATUS from './status';
|
||||
import { run, del } from './sandbox';
|
||||
import { CompileError, SystemError } from './error';
|
||||
import { CompileError } from './error';
|
||||
import { compilerText } from './utils';
|
||||
import { Execute } from './interface';
|
||||
import { getConfig } from './config';
|
||||
|
||||
export = async function compile(
|
||||
lang: string, code: string, target: string, copyIn: any, next?: Function,
|
||||
lang: LangConfig, code: string, target: string, copyIn: any, next?: Function,
|
||||
): Promise<Execute> {
|
||||
const LANGS = yaml.load(getConfig('langs'));
|
||||
if (!LANGS[lang]) throw new SystemError('Unsupported language {0}.', [lang]);
|
||||
const info = LANGS[lang];
|
||||
target = info.target || target;
|
||||
target = lang.target || target;
|
||||
const f = {};
|
||||
if (info.type === 'compiler') {
|
||||
copyIn[info.code_file] = { content: code };
|
||||
if (lang.compile) {
|
||||
copyIn[lang.code_file] = { content: code };
|
||||
const {
|
||||
status, stdout, stderr, fileIds,
|
||||
} = await run(
|
||||
info.compile.replace(/\$\{name\}/g, target),
|
||||
lang.compile.replace(/\$\{name\}/g, target),
|
||||
{ copyIn, copyOutCached: [target] },
|
||||
);
|
||||
if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr });
|
||||
@@ -27,13 +23,11 @@ export = async function compile(
|
||||
if (next) next({ compiler_text: compilerText(stdout, stderr) });
|
||||
f[target] = { fileId: fileIds[target] };
|
||||
return {
|
||||
execute: info.execute, copyIn: f, clean: () => del(fileIds[target]), time: info.time || 1,
|
||||
};
|
||||
} if (info.type === 'interpreter') {
|
||||
f[target] = { content: code };
|
||||
return {
|
||||
execute: info.execute, copyIn: f, clean: () => Promise.resolve(null), time: info.time || 1,
|
||||
execute: lang.execute, copyIn: f, clean: () => del(fileIds[target]), time: lang.time_limit_rate || 1,
|
||||
};
|
||||
}
|
||||
throw new SystemError('Unknown language type.');
|
||||
f[target] = { content: code };
|
||||
return {
|
||||
execute: lang.execute, copyIn: f, clean: () => Promise.resolve(null), time: lang.time_limit_rate || 1,
|
||||
};
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import WebSocket from 'ws';
|
||||
import { argv } from 'yargs';
|
||||
import { noop } from 'lodash';
|
||||
import { ObjectID } from 'bson';
|
||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||
import * as tmpfs from '../tmpfs';
|
||||
import log from '../log';
|
||||
import { compilerText, Queue } from '../utils';
|
||||
@@ -37,6 +38,7 @@ class JudgeTask {
|
||||
config: any;
|
||||
nextId = 1;
|
||||
nextWaiting = [];
|
||||
getLang: (name: string) => LangConfig;
|
||||
|
||||
constructor(session: Hydro, request, ws: WebSocket) {
|
||||
this.stat = {};
|
||||
@@ -45,6 +47,7 @@ class JudgeTask {
|
||||
this.host = session.config.host;
|
||||
this.request = request;
|
||||
this.ws = ws;
|
||||
this.getLang = session.getLang;
|
||||
}
|
||||
|
||||
async handle() {
|
||||
@@ -160,10 +163,9 @@ class JudgeTask {
|
||||
|
||||
export default class Hydro {
|
||||
config: any;
|
||||
|
||||
axios: any;
|
||||
|
||||
ws: WebSocket;
|
||||
language: Record<string, LangConfig>;
|
||||
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
@@ -172,6 +174,7 @@ export default class Hydro {
|
||||
this.config.last_update_at = this.config.last_update_at || 0;
|
||||
if (!this.config.server_url.startsWith('http')) this.config.server_url = `http://${this.config.server_url}`;
|
||||
if (!this.config.server_url.endsWith('/')) this.config.server_url = `${this.config.server_url}/`;
|
||||
this.getLang = this.getLang.bind(this);
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -224,6 +227,11 @@ export default class Hydro {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
getLang(name: string) {
|
||||
if (this.language[name]) return this.language[name];
|
||||
throw new SystemError('Unsupported language {0}', [name]);
|
||||
}
|
||||
|
||||
async consume(queue: Queue<any>) {
|
||||
log.info('正在连接 %sjudge/conn', this.config.server_url);
|
||||
const res = await this.axios.get('judge/conn/info');
|
||||
@@ -238,19 +246,16 @@ export default class Hydro {
|
||||
});
|
||||
this.ws.on('message', (data) => {
|
||||
const request = JSON.parse(data.toString());
|
||||
if (request.language) this.language = request.language;
|
||||
if (request.task) queue.push(new JudgeTask(this, request.task, this.ws));
|
||||
});
|
||||
this.ws.on('close', (data, reason) => {
|
||||
log.warn(`[${this.config.host}] Websocket 断开:`, data, reason);
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
setTimeout(() => this.retry(queue), 30000);
|
||||
});
|
||||
this.ws.on('error', (e) => {
|
||||
log.error(`[${this.config.host}] Websocket 错误:`, e);
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
setTimeout(() => this.retry(queue), 30000);
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
this.ws.once('open', async () => {
|
||||
@@ -327,9 +332,7 @@ export default class Hydro {
|
||||
|
||||
async retry(queue: Queue<any>) {
|
||||
this.consume(queue).catch(() => {
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
setTimeout(() => this.retry(queue), 30000);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,419 +1,7 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import AdmZip from 'adm-zip';
|
||||
import fs from 'fs-extra';
|
||||
import WebSocket from 'ws';
|
||||
import { noop } from 'lodash';
|
||||
import * as tmpfs from '../tmpfs';
|
||||
import log from '../log';
|
||||
import { compilerText, Queue } from '../utils';
|
||||
import { getConfig } from '../config';
|
||||
import { FormatError, CompileError, SystemError } from '../error';
|
||||
import { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } from '../status';
|
||||
import readCases from '../cases';
|
||||
import judge from '../judge';
|
||||
|
||||
class JudgeTask {
|
||||
stat: Record<string, Date>;
|
||||
session: any;
|
||||
host: string;
|
||||
request: any;
|
||||
ws: WebSocket;
|
||||
tag: any;
|
||||
type: any;
|
||||
domain_id: string;
|
||||
pid: string;
|
||||
rid: string;
|
||||
lang: string;
|
||||
code: string;
|
||||
tmpdir: string;
|
||||
clean: Function[];
|
||||
folder: string;
|
||||
config: any;
|
||||
nextId = 1;
|
||||
nextWaiting = [];
|
||||
|
||||
constructor(session: VJ4, request, ws: WebSocket) {
|
||||
this.stat = {};
|
||||
this.stat.receive = new Date();
|
||||
this.session = session;
|
||||
this.host = session.config.host;
|
||||
this.request = request;
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
async handle() {
|
||||
this.next = this.next.bind(this);
|
||||
this.end = this.end.bind(this);
|
||||
this.stat.handle = new Date();
|
||||
this.tag = this.request.tag;
|
||||
this.type = this.request.type;
|
||||
this.domain_id = this.request.domain_id;
|
||||
this.pid = this.request.pid;
|
||||
this.rid = this.request.rid;
|
||||
this.lang = this.request.lang;
|
||||
this.code = this.request.code;
|
||||
this.tmpdir = path.resolve(getConfig('tmp_dir'), 'tmp', this.host, this.rid);
|
||||
this.clean = [];
|
||||
fs.ensureDirSync(this.tmpdir);
|
||||
tmpfs.mount(this.tmpdir, '512m');
|
||||
log.info('Submission: %s/%s/%s pid=%s', this.host, this.domain_id, this.rid, this.pid);
|
||||
try {
|
||||
if (this.type === 0) await this.doSubmission();
|
||||
else if (this.type === 1) await this.doPretest();
|
||||
else throw new SystemError(`Unsupported type: ${this.type}`);
|
||||
} catch (e) {
|
||||
if (e instanceof CompileError) {
|
||||
this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
|
||||
this.end({
|
||||
status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
||||
});
|
||||
} else if (e instanceof FormatError) {
|
||||
this.next({ judge_text: '题目配置出现错误。请联系题目上传者。' });
|
||||
this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` });
|
||||
this.end({
|
||||
status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
||||
});
|
||||
} else {
|
||||
log.error(e);
|
||||
this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` });
|
||||
this.end({
|
||||
status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const clean of this.clean) await clean().catch(noop);
|
||||
tmpfs.umount(this.tmpdir);
|
||||
fs.removeSync(this.tmpdir);
|
||||
}
|
||||
|
||||
async doSubmission() {
|
||||
this.stat.cache_start = new Date();
|
||||
this.folder = await this.session.cacheOpen(this.domain_id, this.pid, this.next);
|
||||
this.stat.read_cases = new Date();
|
||||
this.config = await readCases(
|
||||
this.folder,
|
||||
{ detail: this.session.config.detail },
|
||||
{ next: this.next },
|
||||
);
|
||||
this.stat.judge = new Date();
|
||||
await judge[this.config.type || 'default'].judge(this);
|
||||
}
|
||||
|
||||
async doPretest() {
|
||||
this.folder = path.resolve(this.tmpdir, 'data');
|
||||
await this.session.recordPretestData(this.rid, this.folder);
|
||||
this.config = await readCases(
|
||||
this.folder,
|
||||
{ detail: this.session.config.detail },
|
||||
{ next: this.next },
|
||||
);
|
||||
await judge[this.config.type || 'default'].judge(this);
|
||||
}
|
||||
|
||||
next(data, id?: number) {
|
||||
if (data.message !== undefined) {
|
||||
data.judge_text = data.message;
|
||||
delete data.message;
|
||||
}
|
||||
if (data.judge_text && typeof data.judge_text !== 'string') {
|
||||
data.judge_text = data.judge_text.message.format(data.judge_text.params);
|
||||
}
|
||||
if (data.case?.message !== undefined) {
|
||||
data.case.judge_text = data.case.message;
|
||||
delete data.case.message;
|
||||
}
|
||||
if (data.case?.judge_text && typeof data.case?.judge_text !== 'string') {
|
||||
data.case.judge_text = data.case.judge_text.message.format(data.case.judge_text.params);
|
||||
}
|
||||
data.key = 'next';
|
||||
data.tag = this.tag;
|
||||
if (id) {
|
||||
if (id === this.nextId) {
|
||||
this.ws.send(JSON.stringify(data));
|
||||
this.nextId++;
|
||||
let t = true;
|
||||
while (t) {
|
||||
t = false;
|
||||
for (const i in this.nextWaiting) {
|
||||
if (this.nextId === this.nextWaiting[i].id) {
|
||||
this.ws.send(JSON.stringify(this.nextWaiting[i].data));
|
||||
this.nextId++;
|
||||
this.nextWaiting.splice(+i, 1);
|
||||
t = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else this.nextWaiting.push({ data, id });
|
||||
} else this.ws.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
end(data) {
|
||||
this.ws.send(JSON.stringify({
|
||||
key: 'next',
|
||||
tag: this.tag,
|
||||
judge_text: '自豪地采用 HydroJudge(https://github.com/hydro-dev/Hydro) 进行评测。',
|
||||
}));
|
||||
if (data.message !== undefined) {
|
||||
data.judge_text = data.message;
|
||||
delete data.message;
|
||||
}
|
||||
data.key = 'end';
|
||||
data.tag = this.tag;
|
||||
log.info('Status=%d Score=%s Time=%dms Memory=%dkb', data.status, data.score, data.time_ms, data.memory_kb);
|
||||
this.ws.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
export default class VJ4 {
|
||||
config: any;
|
||||
axios: any;
|
||||
ws: WebSocket;
|
||||
|
||||
constructor(config) {
|
||||
this.config = 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;
|
||||
if (!this.config.server_url.startsWith('http')) this.config.server_url = `http://${this.config.server_url}`;
|
||||
if (!this.config.server_url.endsWith('/')) this.config.server_url = `${this.config.server_url}/`;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.setCookie(this.config.cookie || '');
|
||||
await this.ensureLogin();
|
||||
setInterval(() => { this.axios.get('judge/noop'); }, 30000000);
|
||||
}
|
||||
|
||||
async problemDataVersion(domainId: string, pid: string, retry = 3) {
|
||||
let location: string;
|
||||
let err: Error;
|
||||
await this.ensureLogin();
|
||||
try {
|
||||
await this.axios.get(`d/${domainId}/p/${pid}/data`, { maxRedirects: 0 });
|
||||
} catch (res) {
|
||||
res.response = res.response || {};
|
||||
if (res.response.status === 302) {
|
||||
location = res.response.headers.location;
|
||||
if (location.includes('/fs/')) return location;
|
||||
} else if (res.response.status === 404) {
|
||||
throw new FormatError(`没有找到测试数据 ${domainId}/${pid}`);
|
||||
} else {
|
||||
if (retry) return await this.problemDataVersion(domainId, pid, retry - 1);
|
||||
res.config = res.request = null;
|
||||
err = res;
|
||||
log.error('%o', err);
|
||||
}
|
||||
}
|
||||
if (!location) return 'unknown';
|
||||
try {
|
||||
await this.axios.get(location, { maxRedirects: 0 });
|
||||
} catch (res) {
|
||||
res.response = res.response || {};
|
||||
if (res.response.status === 302) return res.response.headers.location;
|
||||
if (res.response.status === 404) throw new FormatError(`没有找到测试数据 ${domainId}/${pid}`);
|
||||
else {
|
||||
if (retry) return await this.problemDataVersion(domainId, pid, retry - 1);
|
||||
res.config = res.request = null;
|
||||
err = res;
|
||||
log.error(err);
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
async problemData(domainId: string, pid: string, savePath: string, retry = 3, next?) {
|
||||
log.info(`Getting problem data: ${this.config.host}/${domainId}/${pid}`);
|
||||
await this.ensureLogin();
|
||||
if (next) next({ judge_text: '正在同步测试数据,请稍后' });
|
||||
const tmpFilePath = path.resolve(getConfig('cache_dir'), `download_${this.config.host}_${domainId}_${pid}`);
|
||||
try {
|
||||
const res = await this.axios.get(
|
||||
`${this.config.server_url}d/${domainId}/p/${pid}/data`,
|
||||
{ responseType: 'stream' },
|
||||
);
|
||||
const w = fs.createWriteStream(tmpFilePath);
|
||||
res.data.pipe(w);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', reject);
|
||||
});
|
||||
fs.ensureDirSync(path.dirname(savePath));
|
||||
const zip = new AdmZip(tmpFilePath);
|
||||
const entries = zip.getEntries();
|
||||
if (entries.length > 512) throw new FormatError('Too many files');
|
||||
await new Promise((resolve, reject) => {
|
||||
zip.extractAllToAsync(savePath, true, (e) => {
|
||||
if (e) reject(e);
|
||||
else resolve(null);
|
||||
});
|
||||
});
|
||||
await fs.unlink(tmpFilePath);
|
||||
await this.processData(savePath).catch(noop);
|
||||
} catch (e) {
|
||||
if (retry) await this.problemData(domainId, pid, savePath, retry - 1);
|
||||
else throw e;
|
||||
}
|
||||
return savePath;
|
||||
}
|
||||
|
||||
async recordPretestData(rid: string, savePath: string) {
|
||||
log.info(`Getting pretest data: ${this.config.host}/${rid}`);
|
||||
const tmpFilePath = path.resolve(getConfig('cache_dir'), `download_${this.config.host}_${rid}`);
|
||||
await this.ensureLogin();
|
||||
const res = await this.axios.get(`records/${rid}/data`, { responseType: 'stream' });
|
||||
const w = fs.createWriteStream(tmpFilePath);
|
||||
res.data.pipe(w);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', reject);
|
||||
});
|
||||
fs.ensureDirSync(path.dirname(savePath));
|
||||
const zip = new AdmZip(tmpFilePath);
|
||||
const entries = zip.getEntries();
|
||||
if (entries.length > 512) throw new FormatError('Too many files');
|
||||
await new Promise((resolve, reject) => {
|
||||
zip.extractAllToAsync(savePath, true, (e) => {
|
||||
if (e) reject(e);
|
||||
else resolve(null);
|
||||
});
|
||||
});
|
||||
await fs.unlink(tmpFilePath);
|
||||
await this.processData(savePath);
|
||||
return savePath;
|
||||
}
|
||||
|
||||
async consume(queue: Queue<any>) {
|
||||
log.info('正在连接 %sjudge/consume-conn', this.config.server_url);
|
||||
const res = await this.axios.get('judge/consume-conn/info');
|
||||
this.ws = new WebSocket(`${this.config.server_url.replace(/^http/i, 'ws')}judge/consume-conn/websocket?t=${res.data.entropy}`, {
|
||||
headers: { cookie: this.config.cookie },
|
||||
});
|
||||
this.ws.on('message', (data) => {
|
||||
const request = JSON.parse(data.toString());
|
||||
if (!request.event) queue.push(new JudgeTask(this, request, this.ws));
|
||||
});
|
||||
this.ws.on('close', (data, reason) => {
|
||||
log.warn(`[${this.config.host}] Websocket 断开:`, data, reason);
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
});
|
||||
this.ws.on('error', (e) => {
|
||||
log.error(`[${this.config.host}] Websocket 错误:`, e);
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
this.ws.once('open', () => { resolve(null); });
|
||||
});
|
||||
log.info(`[${this.config.host}] 已连接`);
|
||||
}
|
||||
|
||||
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/x-www-form-urlencoded',
|
||||
cookie: this.config.cookie,
|
||||
},
|
||||
transformRequest: [
|
||||
(data) => {
|
||||
let ret = '';
|
||||
for (const it in data) {
|
||||
ret += `${encodeURIComponent(it)}=${encodeURIComponent(data[it])}&`;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
log.info('[%s] Updating session', this.config.host);
|
||||
const res = await this.axios.post('login', {
|
||||
uname: this.config.uname, password: this.config.password, rememberme: 'on',
|
||||
});
|
||||
await this.setCookie(res.headers['set-cookie'].join(';'));
|
||||
}
|
||||
|
||||
async ensureLogin() {
|
||||
try {
|
||||
await this.axios.get('judge/noop');
|
||||
} catch (e) {
|
||||
await this.login();
|
||||
}
|
||||
}
|
||||
|
||||
async processData(folder: string) { // eslint-disable-line class-methods-use-this
|
||||
let files = await fs.readdir(folder);
|
||||
let ini = false;
|
||||
if (files.length <= 2) {
|
||||
if (files.length === 2) files.splice(files.indexOf('version'), 1);
|
||||
const s = fs.statSync(path.resolve(folder, files[0]));
|
||||
if (s.isDirectory()) folder = path.resolve(folder, files[0]);
|
||||
}
|
||||
files = await fs.readdir(folder);
|
||||
for (const i of files) {
|
||||
if (i.toLowerCase() === 'config.ini') {
|
||||
ini = true;
|
||||
await fs.rename(`${folder}/${i}`, `${folder}/config.ini`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ini) {
|
||||
for (const i of files) {
|
||||
if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
|
||||
else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
|
||||
}
|
||||
files = await fs.readdir(`${folder}/input`);
|
||||
for (const i of files) {
|
||||
if (fs.statSync(`${folder}/input/${i}`).size > 64 * 1024 * 1024) {
|
||||
await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
|
||||
} else {
|
||||
const buffer = await fs.readFile(`${folder}/input/${i}`);
|
||||
const data = buffer.toString().replace(/\r/g, '');
|
||||
await fs.unlink(`${folder}/input/${i}`);
|
||||
await fs.writeFile(`${folder}/input/${i.toLowerCase()}`, data);
|
||||
}
|
||||
}
|
||||
files = await fs.readdir(`${folder}/output`);
|
||||
for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async cacheOpen(domainId: string, pid: string, next) {
|
||||
const domainDir = path.join(getConfig('cache_dir'), this.config.host, domainId);
|
||||
const filePath = path.join(domainDir, pid);
|
||||
const version = await this.problemDataVersion(domainId, pid);
|
||||
if (fs.existsSync(filePath)) {
|
||||
let ver;
|
||||
try {
|
||||
ver = fs.readFileSync(path.join(filePath, 'version')).toString();
|
||||
} catch (e) { /* ignore */ }
|
||||
if (version === ver) {
|
||||
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
||||
return filePath;
|
||||
}
|
||||
fs.removeSync(filePath);
|
||||
}
|
||||
fs.ensureDirSync(domainDir);
|
||||
await this.problemData(domainId, pid, filePath, 3, next);
|
||||
fs.writeFileSync(path.join(filePath, 'version'), version);
|
||||
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async retry(queue: Queue<any>) {
|
||||
this.consume(queue).catch(() => {
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
});
|
||||
constructor() {
|
||||
console.log('新版本不再支持 VJ4。请降级至 hydrojudge@2.5.21 或迁移到 Hydro。');
|
||||
console.log('详细文档: https://hydro.js.org/ ');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import Queue from 'p-queue';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
import fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import * as STATUS from '../status';
|
||||
import { CompileError, SystemError } from '../error';
|
||||
import { SystemError } from '../error';
|
||||
import { parseFilename } from '../utils';
|
||||
import { run } from '../sandbox';
|
||||
import compile from '../compile';
|
||||
@@ -128,15 +127,7 @@ function judgeSubtask(subtask, sid) {
|
||||
|
||||
export const judge = async (ctx) => {
|
||||
if (!ctx.config.subtasks.length) throw new SystemError('Problem data not found.');
|
||||
if (ctx.config.template) {
|
||||
if (ctx.config.template[ctx.lang]) {
|
||||
const tpl = ctx.config.template[ctx.lang];
|
||||
ctx.code = tpl[0] + ctx.code + tpl[1];
|
||||
} else throw new CompileError('Language not supported by provided templates');
|
||||
}
|
||||
const LANGS = yaml.load(getConfig('langs'));
|
||||
if (!LANGS[ctx.lang]) throw new SystemError('Unsupported language {0}.', [ctx.lang]);
|
||||
const info = LANGS[ctx.lang];
|
||||
const info = ctx.getLang(ctx.lang);
|
||||
ctx.time_limit_rate = info.time_limit_rate || 1;
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||
[ctx.execute, ctx.checker] = await Promise.all([
|
||||
@@ -145,7 +136,7 @@ export const judge = async (ctx) => {
|
||||
for (const file of ctx.config.user_extra_files) {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next);
|
||||
return await compile(ctx.getLang(ctx.lang), ctx.code, 'code', copyIn, ctx.next);
|
||||
})(),
|
||||
(async () => {
|
||||
if (!ctx.config.checker_type || ctx.config.checker_type === 'default') {
|
||||
@@ -156,6 +147,7 @@ export const judge = async (ctx) => {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compileChecker(
|
||||
ctx.getLang,
|
||||
ctx.config.checker_type,
|
||||
ctx.config.checker,
|
||||
copyIn,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const judge = async (ctx) => {
|
||||
for (const file of ctx.config.user_extra_files) {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next);
|
||||
return await compile(ctx.getLang(ctx.lang), ctx.code, 'code', copyIn, ctx.next);
|
||||
})(),
|
||||
// Validator
|
||||
(async () => {
|
||||
@@ -34,7 +34,7 @@ export const judge = async (ctx) => {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
const file = await fs.readFile(ctx.config.validator);
|
||||
return await compile(parseFilename(ctx.config.validator).split('.')[1], file.toString(), 'validator', copyIn);
|
||||
return await compile(ctx.getLang(parseFilename(ctx.config.validator).split('.')[1]), file.toString(), 'validator', copyIn);
|
||||
})(),
|
||||
// Std
|
||||
(async () => {
|
||||
@@ -43,7 +43,7 @@ export const judge = async (ctx) => {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
const file = await fs.readFile(ctx.config.std);
|
||||
return await compile(parseFilename(ctx.config.std).split('.')[1], file.toString(), 'std', copyIn);
|
||||
return await compile(ctx.getLang(parseFilename(ctx.config.std).split('.')[1]), file.toString(), 'std', copyIn);
|
||||
})(),
|
||||
// Checker
|
||||
(async () => {
|
||||
@@ -53,6 +53,7 @@ export const judge = async (ctx) => {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compileChecker(
|
||||
ctx.getLang,
|
||||
ctx.config.checker_type,
|
||||
ctx.config.checker,
|
||||
copyIn,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Queue from 'p-queue';
|
||||
import fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import yaml from 'js-yaml';
|
||||
import * as STATUS from '../status';
|
||||
import { parse } from '../testlib';
|
||||
import { findFileSync, parseFilename } from '../utils';
|
||||
@@ -9,7 +8,6 @@ import { run } from '../sandbox';
|
||||
import compile from '../compile';
|
||||
import signals from '../signals';
|
||||
import { getConfig } from '../config';
|
||||
import { SystemError } from '../error';
|
||||
|
||||
const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
|
||||
const Score = {
|
||||
@@ -93,27 +91,22 @@ function judgeSubtask(subtask) {
|
||||
|
||||
export const judge = async (ctx) => {
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||
const LANGS = yaml.load(getConfig('langs'));
|
||||
if (!LANGS[ctx.lang]) throw new SystemError('Unsupported language {0}.', [ctx.lang]);
|
||||
const info = LANGS[ctx.lang];
|
||||
ctx.time_limit_rate = info.time_limit_rate || 1;
|
||||
ctx.time_limit_rate = ctx.getLang(ctx.lang).time_limit_rate;
|
||||
[ctx.executeUser, ctx.executeInteractor] = await Promise.all([
|
||||
(async () => {
|
||||
const copyIn = {};
|
||||
for (const file of ctx.config.user_extra_files) {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compile(ctx.lang, ctx.code, 'code', copyIn, ctx.next);
|
||||
return await compile(ctx.getLang(ctx.lang), ctx.code, 'code', copyIn, ctx.next);
|
||||
})(),
|
||||
(async () => {
|
||||
const copyIn = {
|
||||
'testlib.h': testlibSrc,
|
||||
};
|
||||
const copyIn = { 'testlib.h': testlibSrc };
|
||||
for (const file of ctx.config.judge_extra_files) {
|
||||
copyIn[parseFilename(file)] = { src: file };
|
||||
}
|
||||
return await compile(
|
||||
parseFilename(ctx.config.interactor).split('.')[1],
|
||||
ctx.getLang(parseFilename(ctx.config.interactor).split('.')[1]),
|
||||
fs.readFileSync(ctx.config.interactor).toString(),
|
||||
'interactor',
|
||||
copyIn,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const judge = async (ctx) => {
|
||||
ctx.stat.judge = new Date();
|
||||
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||
try {
|
||||
ctx.execute = await compile(ctx.lang, ctx.code, 'code', {}, ctx.next);
|
||||
ctx.execute = await compile(ctx.getLang(ctx.lang), ctx.code, 'code', {}, ctx.next);
|
||||
} catch (e) {
|
||||
if (e instanceof CompileError) {
|
||||
ctx.next({
|
||||
|
||||
@@ -153,6 +153,11 @@ async function postInit() {
|
||||
};
|
||||
}
|
||||
|
||||
function getLang(lang: string) {
|
||||
if (system.lang[lang]) return system.lang[lang];
|
||||
throw new SystemError('Unsupported language {0}.', [lang]);
|
||||
}
|
||||
|
||||
class JudgeTask {
|
||||
stat: any;
|
||||
request: any;
|
||||
@@ -170,11 +175,13 @@ async function postInit() {
|
||||
tmpdir: string;
|
||||
clean: any[];
|
||||
folder: string;
|
||||
getLang: (lang: string) => any;
|
||||
|
||||
constructor(request) {
|
||||
this.stat = {};
|
||||
this.stat.receive = new Date();
|
||||
this.request = request;
|
||||
this.getLang = getLang;
|
||||
logger.debug('%o', request);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ Click here to chat with me: 点击这里与我聊天
|
||||
click to add: 点击添加
|
||||
CLOSE: 关闭
|
||||
Code blocks: 代码区块
|
||||
Code language settings: 编程语言设置
|
||||
Code language: 代码语言
|
||||
Code: 代码
|
||||
collapse: 收缩
|
||||
@@ -416,9 +417,6 @@ Judged At: 评测时间
|
||||
Judged By: 评测机
|
||||
Judging Queue: 评测队列
|
||||
Keep current expiration: 保持当前过期设置
|
||||
LANG_TEXTS: 编译语言代号 -> 语言名称
|
||||
Language file settings: 评测机语言(langs.yaml)
|
||||
Language Highlight ID: 编译语言代号 -> 高亮语言代号
|
||||
Language: 语言
|
||||
last active at: 最后活动于
|
||||
last login at: 最后登录于
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.24.22",
|
||||
"version": "2.25.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "dist/loader.js",
|
||||
"typings": "dist/loader.d.ts",
|
||||
@@ -11,7 +11,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hydrooj/utils": "^1.0.13",
|
||||
"@hydrooj/utils": "^1.0.14",
|
||||
"adm-zip": "^0.5.5",
|
||||
"ansi_up": "^5.0.1",
|
||||
"cookies": "^0.8.0",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
langs:
|
||||
type: yaml
|
||||
desc: Code language settings
|
||||
default:
|
||||
__hint__:
|
||||
- All keys started with _ will be ignored
|
||||
- Entries named like [foo], [foo].[bar] [foo].[baz] will be merged into one category.
|
||||
- A entry named [foo].[bar] will inhert all options defined in entry [foo] if not overwritten
|
||||
- code_file option defaults to foo.[name]
|
||||
- highlight option defaults to [name]
|
||||
- if highlight option was set, monaco option defaults to [highlight], otherwise defaults to [name]
|
||||
- Compile step will be ignored if compile option was not set
|
||||
- Key marked with a ? means the option is optional
|
||||
- (name):
|
||||
compile?: Compile Command
|
||||
code_file?: Where to write code file when compiling
|
||||
execute?: Command to execute the program
|
||||
highlight?: PrismJS highlight mode
|
||||
monaco?: Monaco editor mode
|
||||
time_limit_rate: Time limit rate
|
||||
target: Compiler output file
|
||||
display: Display name
|
||||
c:
|
||||
compile: /usr/bin/gcc -Wall --std=c99 -o ${name} foo.c -lm
|
||||
highlight: c astyle-c
|
||||
monaco: cpp
|
||||
display: C
|
||||
cc:
|
||||
compile: /usr/bin/g++ -Wall -std=c++11 -o ${name} foo.cc -lm
|
||||
code_file: foo.cc
|
||||
highlight: cpp astyle-c
|
||||
monaco: cpp
|
||||
display: C++
|
||||
cc.std98:
|
||||
compile: /usr/bin/g++ -Wall -std=c++98 -o ${name} foo.cc -lm
|
||||
display: C++ 98
|
||||
cc.std11:
|
||||
display: C++ 11
|
||||
cc.std17:
|
||||
compile: /usr/bin/g++ -Wall -std=c++17 -o ${name} foo.cc -lm
|
||||
display: C++ 17
|
||||
pas:
|
||||
compile: /usr/bin/fpc -O2 -o${dir}/${name} foo.pas
|
||||
highlight: pascal
|
||||
display: Pascal
|
||||
java:
|
||||
compile: /usr/bin/javac -d ${dir} -encoding utf8 ./Main.java
|
||||
code_file: Main.java
|
||||
target: Main.class
|
||||
execute: /usr/bin/java Main
|
||||
time_limit_rate: 2
|
||||
highlight: java astyle-java
|
||||
monaco: java
|
||||
display: Java
|
||||
py:
|
||||
compile: /usr/bin/python -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)"
|
||||
code_file: foo.py
|
||||
execute: /usr/bin/python ${name}
|
||||
highlight: python
|
||||
display: Python
|
||||
py.py2:
|
||||
display: Python 2
|
||||
py.py3:
|
||||
compile: /usr/bin/python3 -c "import py_compile; py_compile.compile('${dir}/foo.py', '${dir}/${name}', doraise=True)"
|
||||
execute: /usr/bin/python3 ${name}
|
||||
display: Python 3
|
||||
php:
|
||||
execute: /usr/bin/php foo.php
|
||||
display: PHP
|
||||
rs:
|
||||
compile: /usr/bin/rustc -O -o ${dir}/${name} ${dir}/foo.rs
|
||||
highlight: rust
|
||||
display: Rust
|
||||
hs:
|
||||
compile: /usr/bin/ghc -O -outputdir /tmp -o ${name} foo.hs
|
||||
highlight: haskell
|
||||
display: Haskell
|
||||
js:
|
||||
execute: /usr/bin/jsc ${dir}/foo.js
|
||||
highlight: javascript
|
||||
display: Javascript (JSC)
|
||||
go:
|
||||
compile: /usr/bin/go build -o ${name} foo.go
|
||||
display: Golang
|
||||
rb:
|
||||
execute: /usr/bin/ruby foo.rb
|
||||
highlight: ruby
|
||||
display: Ruby
|
||||
cs:
|
||||
compile: /usr/bin/mcs -optimize+ -out:${dir}/${name} ${dir}/foo.cs
|
||||
execute: /usr/bin/mono ${name}
|
||||
highlight: csharp astyle-cs
|
||||
monaco: csharp
|
||||
display: Csharp
|
||||
jl:
|
||||
execute: /usr/bin/julia ${dir}/foo.jl
|
||||
highlight: julia
|
||||
display: Julia
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { parseLang } from '@hydrooj/utils/lib/lang';
|
||||
import { JudgeResultBody, Rdoc, TestCase } from '../interface';
|
||||
import { sleep } from '../utils';
|
||||
import { Logger } from '../logger';
|
||||
@@ -9,6 +10,7 @@ import * as builtin from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import domain from '../model/domain';
|
||||
import task from '../model/task';
|
||||
import * as system from '../model/system';
|
||||
import * as bus from '../service/bus';
|
||||
import {
|
||||
Route, Handler, Connection, ConnectionHandler, post, Types,
|
||||
@@ -119,12 +121,22 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
||||
processing: any = null;
|
||||
closed = false;
|
||||
query: any = { type: 'judge' };
|
||||
ip: string;
|
||||
|
||||
async prepare() {
|
||||
logger.info('Judge daemon connected from ', this.request.ip);
|
||||
const xff = system.get('server.xff');
|
||||
this.ip = xff ? this.request.headers[xff] || this.request.ip : this.request.ip;
|
||||
logger.info('Judge daemon connected from ', this.ip);
|
||||
this.send({ language: system.lang });
|
||||
this.sendLanguageConfig = this.sendLanguageConfig.bind(this);
|
||||
bus.on('system/setting', this.sendLanguageConfig);
|
||||
this.newTask();
|
||||
}
|
||||
|
||||
async sendLanguageConfig() {
|
||||
this.send({ language: system.lang });
|
||||
}
|
||||
|
||||
async newTask() {
|
||||
if (this.processing) return;
|
||||
let t;
|
||||
@@ -157,7 +169,8 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
logger.info('Judge daemon disconnected from ', this.request.ip);
|
||||
logger.info('Judge daemon disconnected from ', this.ip);
|
||||
bus.off('system/setting', this.sendLanguageConfig);
|
||||
if (this.processing) {
|
||||
await record.reset(this.processing.domainId, this.processing.rid, false);
|
||||
await task.add(this.processing);
|
||||
|
||||
@@ -519,23 +519,6 @@ export const CATEGORIES = {
|
||||
],
|
||||
};
|
||||
|
||||
export const LANG_TEXTS = {
|
||||
c: 'C',
|
||||
cc: 'C++',
|
||||
cs: 'C#',
|
||||
pas: 'Pascal',
|
||||
java: 'Java',
|
||||
py: 'Python',
|
||||
py3: 'Python 3',
|
||||
php: 'PHP',
|
||||
rs: 'Rust',
|
||||
hs: 'Haskell',
|
||||
js: 'JavaScript',
|
||||
go: 'Go',
|
||||
rb: 'Ruby',
|
||||
jl: 'Julia',
|
||||
};
|
||||
|
||||
global.Hydro.model.builtin = {
|
||||
Permission,
|
||||
PERM,
|
||||
@@ -547,7 +530,6 @@ global.Hydro.model.builtin = {
|
||||
BUILTIN_ROLES,
|
||||
DEFAULT_NODES,
|
||||
CATEGORIES,
|
||||
LANG_TEXTS,
|
||||
STATUS,
|
||||
STATUS_TEXTS,
|
||||
STATUS_CODES,
|
||||
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment-timezone';
|
||||
import { Dictionary } from 'lodash';
|
||||
import yaml from 'js-yaml';
|
||||
import { retry } from '@hydrooj/utils/lib/utils';
|
||||
import { parseLang } from '@hydrooj/utils/lib/lang';
|
||||
import * as builtin from './builtin';
|
||||
import { Setting as _Setting } from '../interface';
|
||||
import { Logger } from '../logger';
|
||||
@@ -98,10 +99,30 @@ export const SystemSetting = (...settings: _Setting[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const LangSettingNode = {
|
||||
family: 'setting_usage',
|
||||
key: 'codeLang',
|
||||
value: 'c',
|
||||
name: 'codeLang',
|
||||
desc: 'Default Code Language',
|
||||
flag: 0,
|
||||
subType: '',
|
||||
type: 'select',
|
||||
range: {},
|
||||
};
|
||||
|
||||
bus.on('system/setting', (args) => {
|
||||
if (!args['hydrooj.lang']) return;
|
||||
const lang = parseLang(args['hydrooj.lang']);
|
||||
const range = {};
|
||||
for (const key in lang) range[key] = lang[key].display;
|
||||
LangSettingNode.range = range;
|
||||
});
|
||||
|
||||
PreferenceSetting(
|
||||
Setting('setting_display', 'viewLang', null, langRange, 'UI Language'),
|
||||
Setting('setting_display', 'timeZone', 'Asia/Shanghai', timezones, 'Timezone'),
|
||||
Setting('setting_usage', 'codeLang', 'c', builtin.LANG_TEXTS, 'Default Code Language'),
|
||||
LangSettingNode,
|
||||
Setting('setting_usage', 'codeTemplate', '', 'textarea', 'Default Code Template',
|
||||
'If left blank, the built-in template of the corresponding language will be used.'),
|
||||
);
|
||||
@@ -165,7 +186,6 @@ SystemSetting(
|
||||
Setting('setting_limits', 'limit.problem_files_max', 100, 'number', 'limit.problem_files_max', 'Max files per problem'),
|
||||
Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege'),
|
||||
Setting('setting_basic', 'problem.categories', builtin.CATEGORIES, 'yaml', 'problem.categories', 'Problem Categories'),
|
||||
Setting('setting_basic', 'lang.texts', builtin.LANG_TEXTS, 'yaml', 'lang.texts', 'LANG_TEXTS'),
|
||||
Setting('setting_basic', 'pagination.problem', 100, 'number', 'pagination.problem', 'Problems per page'),
|
||||
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests per page'),
|
||||
Setting('setting_basic', 'pagination.discussion', 50, 'number', 'pagination.discussion', 'Discussions per page'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseLang } from '@hydrooj/utils/lib/lang';
|
||||
import { SYSTEM_SETTINGS } from './setting';
|
||||
import { NumberKeys } from '../typeutils';
|
||||
import { SystemKeys } from '../interface';
|
||||
@@ -70,8 +71,13 @@ export async function runConfig() {
|
||||
await bus.emit('database/config');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let lang = {};
|
||||
|
||||
bus.on('system/setting', (args) => {
|
||||
for (const key in args) set(key, args[key], false);
|
||||
if (!args['hydrooj.lang']) return;
|
||||
lang = parseLang(args['hydrooj.lang']);
|
||||
});
|
||||
|
||||
global.Hydro.model.system = {
|
||||
@@ -80,4 +86,5 @@ global.Hydro.model.system = {
|
||||
getMany,
|
||||
inc,
|
||||
set,
|
||||
lang,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
value: state.editor.code,
|
||||
language: window.MONACO_MODES[state.editor.lang],
|
||||
language: window.LANGS[state.editor.lang].monaco,
|
||||
theme: 'vs-light',
|
||||
mainSize: state.ui.main.size,
|
||||
pretestSize: state.ui.pretest.size,
|
||||
|
||||
@@ -136,8 +136,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
|
||||
value={this.props.editorLang}
|
||||
onChange={(ev) => this.props.setEditorLanguage(ev.target.value)}
|
||||
>
|
||||
{_.map(window.LANG_TEXTS, (val, key) => (
|
||||
<option value={key} key={key}>{val}</option>
|
||||
{_.map(window.LANGS, (val, key) => (
|
||||
<option value={key} key={key}>{val.display}</option>
|
||||
))}
|
||||
</select>
|
||||
</ToolbarItem>
|
||||
|
||||
@@ -53,15 +53,7 @@ class UiConstantsHandler extends Handler {
|
||||
}
|
||||
|
||||
async get() {
|
||||
const [LANG_TEXTS, LANG_HIGHLIGHT_ID, MONACO_MODES] = system.getMany([
|
||||
'lang.texts',
|
||||
'ui-default.lang_highlight_id',
|
||||
'ui-default.lang_monaco_modes',
|
||||
]);
|
||||
this.response.body = `\
|
||||
window.LANG_TEXTS=${JSON.stringify(yaml.load(LANG_TEXTS))};
|
||||
window.LANG_HIGHLIGHT_ID=${JSON.stringify(yaml.load(LANG_HIGHLIGHT_ID))};
|
||||
window.MONACO_MODES=${JSON.stringify(yaml.load(MONACO_MODES))}`;
|
||||
this.response.body = `window.LANGS=${JSON.stringify(system.langs)}`;
|
||||
this.response.type = 'text/javascript';
|
||||
this.ctx.set('nolog', '1');
|
||||
}
|
||||
|
||||
@@ -433,7 +433,6 @@ Memory: 内存
|
||||
Message {0} not found.: 消息 {0} 不存在。
|
||||
Messages: 消息
|
||||
Method: 方法
|
||||
Monaco language modes: 编译语言代号 -> Monaco 编辑器模式
|
||||
Month: 月
|
||||
Monthly Popular: 月度最受欢迎
|
||||
Most Upvoted Solutions: 最被赞同的题解
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.7.8",
|
||||
"version": "4.8.0",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
@@ -22,7 +22,7 @@
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@babel/register": "^7.13.14",
|
||||
"@babel/runtime-corejs2": "^7.14.0",
|
||||
"@hydrooj/utils": "^1.0.13",
|
||||
"@hydrooj/utils": "^1.0.14",
|
||||
"ansi_up": "^5.0.1",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
||||
@@ -43,44 +43,6 @@ nav_logo_dark_2x:
|
||||
name: nav_logo_dark_2x
|
||||
desc: Nav Logo Dark@2x
|
||||
default: /components/navigation/nav-logo-small@2x_dark.png
|
||||
lang_highlight_id:
|
||||
type: yaml
|
||||
name: lang_highlight_id
|
||||
desc: Language Highlight ID
|
||||
default:
|
||||
c: c astyle-c
|
||||
cc: cpp astyle-c
|
||||
cs: csharp astyle-cs
|
||||
pas: pascal
|
||||
java: java astyle-java
|
||||
py: python
|
||||
py3: python
|
||||
php: php
|
||||
rs: rust
|
||||
hs: haskell
|
||||
js: javascript
|
||||
go: go
|
||||
rb: ruby
|
||||
jl: julia
|
||||
lang_monaco_modes:
|
||||
type: yaml
|
||||
name: lang_monaco_modes
|
||||
desc: Monaco language modes
|
||||
default:
|
||||
c: cpp
|
||||
cc: cpp
|
||||
cs: csharp
|
||||
pas: pascal
|
||||
java: java
|
||||
py: python
|
||||
py3: python
|
||||
php: php
|
||||
rs: rust
|
||||
hs: haskell
|
||||
js: javascript
|
||||
go: go
|
||||
rb: ruby
|
||||
jl: julia
|
||||
preferredEditorType:
|
||||
type:
|
||||
wysiwyg: WYSIWYG
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<h1 class="section__title">{{ _('Code') }}</h1>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<pre><code class="line-numbers language-{{ (model.system.get('ui-default.lang_highlight_id')|parseYaml)[rdoc['lang']] }}">{{ rdoc['code'] }}</code></pre>
|
||||
<pre><code class="line-numbers language-{{ model.system.langs[rdoc.lang].highlight }}">{{ rdoc['code'] }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export interface LangConfig {
|
||||
compile?: string;
|
||||
execute: string;
|
||||
code_file: string;
|
||||
highlight: string;
|
||||
monaco: string;
|
||||
time_limit_rate: number;
|
||||
display: string;
|
||||
target?: string;
|
||||
sub?: LangConfig[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
export function parseLang(config: string): Record<string, LangConfig> {
|
||||
const file = yaml.load(config) as Record<string, LangConfig>;
|
||||
if (typeof file === 'undefined' || typeof file === 'string' || typeof file === 'number') throw new Error();
|
||||
Object.keys(file).filter((i) => i.startsWith('_')).forEach((k) => delete file[k]);
|
||||
for (const key in file) {
|
||||
const entry = file[key];
|
||||
if (key.includes('.')) {
|
||||
const base = key.split('.')[0];
|
||||
const baseCfg = file[base];
|
||||
if (baseCfg) {
|
||||
for (const bkey in baseCfg) {
|
||||
if (!(bkey in entry)) entry[bkey] = baseCfg[bkey];
|
||||
}
|
||||
if (baseCfg.sub) baseCfg.sub.push(entry);
|
||||
else baseCfg.sub = [entry];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in file) {
|
||||
const entry = file[key];
|
||||
entry.highlight = entry.highlight || key;
|
||||
entry.monaco = entry.monaco || entry.highlight;
|
||||
entry.time_limit_rate = entry.time_limit_rate || 1;
|
||||
entry.code_file = entry.code_file || `foo.${key}`;
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
entry.execute = entry.execute || '${dir}/${name}';
|
||||
entry.key = key;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/utils",
|
||||
"version": "1.0.13",
|
||||
"version": "1.0.14",
|
||||
"description": "hydrooj utils",
|
||||
"main": "package.json",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
|
||||
Reference in new issue
Block a user