Merge branch 'develop'

This commit is contained in:
undefined committed 2021-05-08 00:29:28 +08:00
commit b3e558a376
36 files changed
+404 -692

No files matched your search

+8 -12
View File
@@ -6,14 +6,13 @@ let MINIO_ACCESS_KEY = randomstring(32);
let MINIO_SECRET_KEY = randomstring(32); let MINIO_SECRET_KEY = randomstring(32);
let DATABASE_PASSWORD = randomstring(32); let DATABASE_PASSWORD = randomstring(32);
const default_nvm_source_command = `\ const source_nvm = `\
# load nvm env (by hydro installer) # load nvm env (by hydro installer)
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"`;
`;
if (__arch !== 'amd64') log.fatal('不支持的架构 %s ,请尝试手动安装', __arch); if (__arch !== 'amd64') log.fatal('不支持的架构 %s ,请尝试手动安装', __arch);
const china = (cli.prompt('此服务器是否位于中国大陆?(Y/n) ') || 'y') === 'y'; const china = (cli.prompt('此服务器是否位于中国大陆?(Y/n) ') || 'y').toLowerCase().trim() === 'y';
const NVM_NODEJS_ORG_MIRROR = china const NVM_NODEJS_ORG_MIRROR = china
? 'https://mirrors.tuna.tsinghua.edu.cn/nodejs-release' ? 'https://mirrors.tuna.tsinghua.edu.cn/nodejs-release'
: 'https://nodejs.org/dist'; : 'https://nodejs.org/dist';
@@ -34,7 +33,7 @@ const values = {};
for (const line of lines) { for (const line of lines) {
if (!line.trim()) continue; if (!line.trim()) continue;
const d = line.split('='); const d = line.split('=');
if (d[1].startsWith('"')) values[d[0]] = d[1].substr(1, d[1].length - 2); if (d[1].startsWith('"')) values[d[0].toLowerCase()] = d[1].substr(1, d[1].length - 2);
else values[d[0].toLowerCase()] = d[1]; else values[d[0].toLowerCase()] = d[1];
} }
if (!['ubuntu', 'arch'].includes(values.id)) log.fatal('不支持的系统'); if (!['ubuntu', 'arch'].includes(values.id)) log.fatal('不支持的系统');
@@ -64,7 +63,7 @@ const steps = [
() => { () => {
fs.writefile( fs.writefile(
'/etc/apt/sources.list.d/mongodb-org-4.4.list', '/etc/apt/sources.list.d/mongodb-org-4.4.list',
`deb [ arch=amd64 ] ${MONGODB_REPO} ${values.UBUNTU_CODENAME}/mongodb-org/4.4 multiverse`, `deb [ arch=amd64 ] ${MONGODB_REPO} ${values.ubuntu_codename}/mongodb-org/4.4 multiverse`,
); );
}, },
'wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -', 'wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -',
@@ -97,13 +96,10 @@ const steps = [
const ver = res.output.split('Now using node v')[1].split(' ')[0]; const ver = res.output.split('Now using node v')[1].split(' ')[0];
setenv('PATH', `/root/.nvm/versions/node/v${ver}/bin:${__env.PATH}`); setenv('PATH', `/root/.nvm/versions/node/v${ver}/bin:${__env.PATH}`);
const rc_path = `/root/.${__env['SHELL'].endsWith('zsh') ? 'zsh' : 'bash'}rc`; const rc_path = `/root/.${__env['SHELL'].endsWith('zsh') ? 'zsh' : 'bash'}rc`;
if (!fs.exist(rc_path)) { if (!fs.exist(rc_path)) fs.writefile(rc_path, source_nvm);
fs.writefile(rc_path, default_nvm_source_command); else {
} else {
const rc_file = fs.readfile(rc_path); const rc_file = fs.readfile(rc_path);
if (!rc_file.includes(default_nvm_source_command)) { if (!rc_file.includes(source_nvm)) fs.appendfile(rc_path, source_nvm);
fs.appendfile(default_nvm_source_command);
}
} }
}, },
'npm i yarn -g', 'npm i yarn -g',
+2 -3
View File
@@ -1,13 +1,12 @@
{ {
"name": "@hydrooj/hydrojudge", "name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js", "bin": "bin/hydrojudge.js",
"version": "2.5.21", "version": "2.6.0",
"main": "package.json", "main": "package.json",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
"dependencies": { "dependencies": {
"@hydrooj/utils": "^1.0.9", "@hydrooj/utils": "^1.0.14",
"adm-zip": "^0.5.5",
"axios": "^0.21.1", "axios": "^0.21.1",
"bson": "^4.3.0", "bson": "^4.3.0",
"fs-extra": "^9.1.0", "fs-extra": "^9.1.0",
-98
View File
@@ -52,101 +52,3 @@ disable:
name: disable name: disable
desc: Disable builtin judge desc: Disable builtin judge
default: false 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
+2 -2
View File
@@ -24,10 +24,10 @@ export async function check(config): Promise<[number, number, string]> {
return [status, score, message]; 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 (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]);
if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc }; if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc };
const file = await fs.readFile(checker); const file = await fs.readFile(checker);
// TODO cache compiled 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);
} }
+12 -18
View File
@@ -1,25 +1,21 @@
import yaml from 'js-yaml'; import { LangConfig } from '@hydrooj/utils/lib/lang';
import * as STATUS from './status'; import * as STATUS from './status';
import { run, del } from './sandbox'; import { run, del } from './sandbox';
import { CompileError, SystemError } from './error'; import { CompileError } from './error';
import { compilerText } from './utils'; import { compilerText } from './utils';
import { Execute } from './interface'; import { Execute } from './interface';
import { getConfig } from './config';
export = async function compile( 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> { ): Promise<Execute> {
const LANGS = yaml.load(getConfig('langs')); target = lang.target || target;
if (!LANGS[lang]) throw new SystemError('Unsupported language {0}.', [lang]);
const info = LANGS[lang];
target = info.target || target;
const f = {}; const f = {};
if (info.type === 'compiler') { if (lang.compile) {
copyIn[info.code_file] = { content: code }; copyIn[lang.code_file] = { content: code };
const { const {
status, stdout, stderr, fileIds, status, stdout, stderr, fileIds,
} = await run( } = await run(
info.compile.replace(/\$\{name\}/g, target), lang.compile.replace(/\$\{name\}/g, target),
{ copyIn, copyOutCached: [target] }, { copyIn, copyOutCached: [target] },
); );
if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr }); 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) }); if (next) next({ compiler_text: compilerText(stdout, stderr) });
f[target] = { fileId: fileIds[target] }; f[target] = { fileId: fileIds[target] };
return { return {
execute: info.execute, copyIn: f, clean: () => del(fileIds[target]), time: info.time || 1, execute: lang.execute, copyIn: f, clean: () => del(fileIds[target]), time: lang.time_limit_rate || 1,
};
} if (info.type === 'interpreter') {
f[target] = { content: code };
return {
execute: info.execute, copyIn: f, clean: () => Promise.resolve(null), time: info.time || 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,
};
}; };
+14 -11
View File
@@ -6,6 +6,7 @@ import WebSocket from 'ws';
import { argv } from 'yargs'; import { argv } from 'yargs';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { ObjectID } from 'bson'; import { ObjectID } from 'bson';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import * as tmpfs from '../tmpfs'; import * as tmpfs from '../tmpfs';
import log from '../log'; import log from '../log';
import { compilerText, Queue } from '../utils'; import { compilerText, Queue } from '../utils';
@@ -37,6 +38,7 @@ class JudgeTask {
config: any; config: any;
nextId = 1; nextId = 1;
nextWaiting = []; nextWaiting = [];
getLang: (name: string) => LangConfig;
constructor(session: Hydro, request, ws: WebSocket) { constructor(session: Hydro, request, ws: WebSocket) {
this.stat = {}; this.stat = {};
@@ -45,6 +47,7 @@ class JudgeTask {
this.host = session.config.host; this.host = session.config.host;
this.request = request; this.request = request;
this.ws = ws; this.ws = ws;
this.getLang = session.getLang;
} }
async handle() { async handle() {
@@ -160,10 +163,9 @@ class JudgeTask {
export default class Hydro { export default class Hydro {
config: any; config: any;
axios: any; axios: any;
ws: WebSocket; ws: WebSocket;
language: Record<string, LangConfig>;
constructor(config) { constructor(config) {
this.config = config; this.config = config;
@@ -172,6 +174,7 @@ export default class Hydro {
this.config.last_update_at = this.config.last_update_at || 0; 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.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}/`; if (!this.config.server_url.endsWith('/')) this.config.server_url = `${this.config.server_url}/`;
this.getLang = this.getLang.bind(this);
} }
async init() { async init() {
@@ -224,6 +227,11 @@ export default class Hydro {
return filePath; 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>) { async consume(queue: Queue<any>) {
log.info('正在连接 %sjudge/conn', this.config.server_url); log.info('正在连接 %sjudge/conn', this.config.server_url);
const res = await this.axios.get('judge/conn/info'); const res = await this.axios.get('judge/conn/info');
@@ -238,19 +246,16 @@ export default class Hydro {
}); });
this.ws.on('message', (data) => { this.ws.on('message', (data) => {
const request = JSON.parse(data.toString()); 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)); if (request.task) queue.push(new JudgeTask(this, request.task, this.ws));
}); });
this.ws.on('close', (data, reason) => { this.ws.on('close', (data, reason) => {
log.warn(`[${this.config.host}] Websocket 断开:`, data, reason); log.warn(`[${this.config.host}] Websocket 断开:`, data, reason);
setTimeout(() => { setTimeout(() => this.retry(queue), 30000);
this.retry(queue);
}, 30000);
}); });
this.ws.on('error', (e) => { this.ws.on('error', (e) => {
log.error(`[${this.config.host}] Websocket 错误:`, e); log.error(`[${this.config.host}] Websocket 错误:`, e);
setTimeout(() => { setTimeout(() => this.retry(queue), 30000);
this.retry(queue);
}, 30000);
}); });
await new Promise((resolve) => { await new Promise((resolve) => {
this.ws.once('open', async () => { this.ws.once('open', async () => {
@@ -327,9 +332,7 @@ export default class Hydro {
async retry(queue: Queue<any>) { async retry(queue: Queue<any>) {
this.consume(queue).catch(() => { this.consume(queue).catch(() => {
setTimeout(() => { setTimeout(() => this.retry(queue), 30000);
this.retry(queue);
}, 30000);
}); });
} }
} }
+4 -416
View File
@@ -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 { export default class VJ4 {
config: any; constructor() {
axios: any; console.log('新版本不再支持 VJ4。请降级至 hydrojudge@2.5.21 或迁移到 Hydro。');
ws: WebSocket; console.log('详细文档: https://hydro.js.org/ ');
process.exit(1);
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);
});
} }
} }
+4 -12
View File
@@ -1,10 +1,9 @@
import Queue from 'p-queue'; import Queue from 'p-queue';
import path from 'path'; import path from 'path';
import yaml from 'js-yaml';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { argv } from 'yargs'; import { argv } from 'yargs';
import * as STATUS from '../status'; import * as STATUS from '../status';
import { CompileError, SystemError } from '../error'; import { SystemError } from '../error';
import { parseFilename } from '../utils'; import { parseFilename } from '../utils';
import { run } from '../sandbox'; import { run } from '../sandbox';
import compile from '../compile'; import compile from '../compile';
@@ -128,15 +127,7 @@ function judgeSubtask(subtask, sid) {
export const judge = async (ctx) => { export const judge = async (ctx) => {
if (!ctx.config.subtasks.length) throw new SystemError('Problem data not found.'); if (!ctx.config.subtasks.length) throw new SystemError('Problem data not found.');
if (ctx.config.template) { const info = ctx.getLang(ctx.lang);
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];
ctx.time_limit_rate = info.time_limit_rate || 1; ctx.time_limit_rate = info.time_limit_rate || 1;
ctx.next({ status: STATUS.STATUS_COMPILING }); ctx.next({ status: STATUS.STATUS_COMPILING });
[ctx.execute, ctx.checker] = await Promise.all([ [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) { for (const file of ctx.config.user_extra_files) {
copyIn[parseFilename(file)] = { src: file }; 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 () => { (async () => {
if (!ctx.config.checker_type || ctx.config.checker_type === 'default') { if (!ctx.config.checker_type || ctx.config.checker_type === 'default') {
@@ -156,6 +147,7 @@ export const judge = async (ctx) => {
copyIn[parseFilename(file)] = { src: file }; copyIn[parseFilename(file)] = { src: file };
} }
return await compileChecker( return await compileChecker(
ctx.getLang,
ctx.config.checker_type, ctx.config.checker_type,
ctx.config.checker, ctx.config.checker,
copyIn, copyIn,
+4 -3
View File
@@ -25,7 +25,7 @@ export const judge = async (ctx) => {
for (const file of ctx.config.user_extra_files) { for (const file of ctx.config.user_extra_files) {
copyIn[parseFilename(file)] = { src: file }; 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 // Validator
(async () => { (async () => {
@@ -34,7 +34,7 @@ export const judge = async (ctx) => {
copyIn[parseFilename(file)] = { src: file }; copyIn[parseFilename(file)] = { src: file };
} }
const file = await fs.readFile(ctx.config.validator); 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 // Std
(async () => { (async () => {
@@ -43,7 +43,7 @@ export const judge = async (ctx) => {
copyIn[parseFilename(file)] = { src: file }; copyIn[parseFilename(file)] = { src: file };
} }
const file = await fs.readFile(ctx.config.std); 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 // Checker
(async () => { (async () => {
@@ -53,6 +53,7 @@ export const judge = async (ctx) => {
copyIn[parseFilename(file)] = { src: file }; copyIn[parseFilename(file)] = { src: file };
} }
return await compileChecker( return await compileChecker(
ctx.getLang,
ctx.config.checker_type, ctx.config.checker_type,
ctx.config.checker, ctx.config.checker,
copyIn, copyIn,
+4 -11
View File
@@ -1,7 +1,6 @@
import Queue from 'p-queue'; import Queue from 'p-queue';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { argv } from 'yargs'; import { argv } from 'yargs';
import yaml from 'js-yaml';
import * as STATUS from '../status'; import * as STATUS from '../status';
import { parse } from '../testlib'; import { parse } from '../testlib';
import { findFileSync, parseFilename } from '../utils'; import { findFileSync, parseFilename } from '../utils';
@@ -9,7 +8,6 @@ import { run } from '../sandbox';
import compile from '../compile'; import compile from '../compile';
import signals from '../signals'; import signals from '../signals';
import { getConfig } from '../config'; import { getConfig } from '../config';
import { SystemError } from '../error';
const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'); const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
const Score = { const Score = {
@@ -93,27 +91,22 @@ function judgeSubtask(subtask) {
export const judge = async (ctx) => { export const judge = async (ctx) => {
ctx.next({ status: STATUS.STATUS_COMPILING }); ctx.next({ status: STATUS.STATUS_COMPILING });
const LANGS = yaml.load(getConfig('langs')); ctx.time_limit_rate = ctx.getLang(ctx.lang).time_limit_rate;
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.executeUser, ctx.executeInteractor] = await Promise.all([ [ctx.executeUser, ctx.executeInteractor] = await Promise.all([
(async () => { (async () => {
const copyIn = {}; const copyIn = {};
for (const file of ctx.config.user_extra_files) { for (const file of ctx.config.user_extra_files) {
copyIn[parseFilename(file)] = { src: file }; 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 () => { (async () => {
const copyIn = { const copyIn = { 'testlib.h': testlibSrc };
'testlib.h': testlibSrc,
};
for (const file of ctx.config.judge_extra_files) { for (const file of ctx.config.judge_extra_files) {
copyIn[parseFilename(file)] = { src: file }; copyIn[parseFilename(file)] = { src: file };
} }
return await compile( return await compile(
parseFilename(ctx.config.interactor).split('.')[1], ctx.getLang(parseFilename(ctx.config.interactor).split('.')[1]),
fs.readFileSync(ctx.config.interactor).toString(), fs.readFileSync(ctx.config.interactor).toString(),
'interactor', 'interactor',
copyIn, copyIn,
+1 -1
View File
@@ -12,7 +12,7 @@ export const judge = async (ctx) => {
ctx.stat.judge = new Date(); ctx.stat.judge = new Date();
ctx.next({ status: STATUS.STATUS_COMPILING }); ctx.next({ status: STATUS.STATUS_COMPILING });
try { 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) { } catch (e) {
if (e instanceof CompileError) { if (e instanceof CompileError) {
ctx.next({ ctx.next({
+8 -1
View File
@@ -39,7 +39,7 @@ async function postInit() {
if (!global.Hydro.isFirstWorker) return; if (!global.Hydro.isFirstWorker) return;
const judge = require('./judge'); const judge = require('./judge');
const { task, system } = global.Hydro.model; const { task, system, setting } = global.Hydro.model;
const { storage } = global.Hydro.service; const { storage } = global.Hydro.service;
const _judge = global.Hydro.handler.judge as any; const _judge = global.Hydro.handler.judge as any;
@@ -153,6 +153,11 @@ async function postInit() {
}; };
} }
function getLang(lang: string) {
if (setting.langs[lang]) return setting.langs[lang];
throw new SystemError('Unsupported language {0}.', [lang]);
}
class JudgeTask { class JudgeTask {
stat: any; stat: any;
request: any; request: any;
@@ -170,11 +175,13 @@ async function postInit() {
tmpdir: string; tmpdir: string;
clean: any[]; clean: any[];
folder: string; folder: string;
getLang: (lang: string) => any;
constructor(request) { constructor(request) {
this.stat = {}; this.stat = {};
this.stat.receive = new Date(); this.stat.receive = new Date();
this.request = request; this.request = request;
this.getLang = getLang;
logger.debug('%o', request); logger.debug('%o', request);
} }
+1 -3
View File
@@ -125,6 +125,7 @@ Click here to chat with me: 点击这里与我聊天
click to add: 点击添加 click to add: 点击添加
CLOSE: 关闭 CLOSE: 关闭
Code blocks: 代码区块 Code blocks: 代码区块
Code language settings: 编程语言设置
Code language: 代码语言 Code language: 代码语言
Code: 代码 Code: 代码
collapse: 收缩 collapse: 收缩
@@ -416,9 +417,6 @@ Judged At: 评测时间
Judged By: 评测机 Judged By: 评测机
Judging Queue: 评测队列 Judging Queue: 评测队列
Keep current expiration: 保持当前过期设置 Keep current expiration: 保持当前过期设置
LANG_TEXTS: 编译语言代号 -> 语言名称
Language file settings: 评测机语言(langs.yaml)
Language Highlight ID: 编译语言代号 -> 高亮语言代号
Language: 语言 Language: 语言
last active at: 最后活动于 last active at: 最后活动于
last login at: 最后登录于 last login at: 最后登录于
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.24.22", "version": "2.25.2",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "dist/loader.js", "main": "dist/loader.js",
"typings": "dist/loader.d.ts", "typings": "dist/loader.d.ts",
@@ -11,7 +11,7 @@
"node": ">=14" "node": ">=14"
}, },
"dependencies": { "dependencies": {
"@hydrooj/utils": "^1.0.13", "@hydrooj/utils": "^1.0.14",
"adm-zip": "^0.5.5", "adm-zip": "^0.5.5",
"ansi_up": "^5.0.1", "ansi_up": "^5.0.1",
"cookies": "^0.8.0", "cookies": "^0.8.0",
+101
View File
@@ -0,0 +1,101 @@
langs:
type: yaml
desc: Code language settings
default:
__hint__:
- ~ All keys starting 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]
- ~ Compiling be be ignored if compile option wasn't 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
View File
@@ -115,6 +115,7 @@ export async function setting(pending: string[], fail: string[], modelSetting: t
), ),
); );
} }
logger.info('Config load: %s', i);
} catch (e) { } catch (e) {
logger.error('Config Load Fail: %s', i); logger.error('Config Load Fail: %s', i);
logger.error(e); logger.error(e);
+15 -2
View File
@@ -5,10 +5,12 @@ import { Logger } from '../logger';
import difficultyAlgorithm from '../lib/difficulty'; import difficultyAlgorithm from '../lib/difficulty';
import record from '../model/record'; import record from '../model/record';
import problem from '../model/problem'; import problem from '../model/problem';
import * as setting from '../model/setting';
import * as builtin from '../model/builtin'; import * as builtin from '../model/builtin';
import * as contest from '../model/contest'; import * as contest from '../model/contest';
import domain from '../model/domain'; import domain from '../model/domain';
import task from '../model/task'; import task from '../model/task';
import * as system from '../model/system';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import { import {
Route, Handler, Connection, ConnectionHandler, post, Types, Route, Handler, Connection, ConnectionHandler, post, Types,
@@ -119,12 +121,22 @@ class JudgeConnectionHandler extends ConnectionHandler {
processing: any = null; processing: any = null;
closed = false; closed = false;
query: any = { type: 'judge' }; query: any = { type: 'judge' };
ip: string;
async prepare() { 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: setting.langs });
this.sendLanguageConfig = this.sendLanguageConfig.bind(this);
bus.on('system/setting', this.sendLanguageConfig);
this.newTask(); this.newTask();
} }
async sendLanguageConfig() {
this.send({ language: setting.langs });
}
async newTask() { async newTask() {
if (this.processing) return; if (this.processing) return;
let t; let t;
@@ -157,7 +169,8 @@ class JudgeConnectionHandler extends ConnectionHandler {
} }
async cleanup() { 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) { if (this.processing) {
await record.reset(this.processing.domainId, this.processing.rid, false); await record.reset(this.processing.domainId, this.processing.rid, false);
await task.add(this.processing); await task.add(this.processing);
-18
View File
@@ -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 = { global.Hydro.model.builtin = {
Permission, Permission,
PERM, PERM,
@@ -547,7 +530,6 @@ global.Hydro.model.builtin = {
BUILTIN_ROLES, BUILTIN_ROLES,
DEFAULT_NODES, DEFAULT_NODES,
CATEGORIES, CATEGORIES,
LANG_TEXTS,
STATUS, STATUS,
STATUS_TEXTS, STATUS_TEXTS,
STATUS_CODES, STATUS_CODES,
+30 -5
View File
@@ -3,6 +3,7 @@ import moment from 'moment-timezone';
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { retry } from '@hydrooj/utils/lib/utils'; import { retry } from '@hydrooj/utils/lib/utils';
import { parseLang } from '@hydrooj/utils/lib/lang';
import * as builtin from './builtin'; import * as builtin from './builtin';
import { Setting as _Setting } from '../interface'; import { Setting as _Setting } from '../interface';
import { Logger } from '../logger'; import { Logger } from '../logger';
@@ -98,10 +99,22 @@ 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: {},
};
PreferenceSetting( PreferenceSetting(
Setting('setting_display', 'viewLang', null, langRange, 'UI Language'), Setting('setting_display', 'viewLang', null, langRange, 'UI Language'),
Setting('setting_display', 'timeZone', 'Asia/Shanghai', timezones, 'Timezone'), 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', Setting('setting_usage', 'codeTemplate', '', 'textarea', 'Default Code Template',
'If left blank, the built-in template of the corresponding language will be used.'), 'If left blank, the built-in template of the corresponding language will be used.'),
); );
@@ -165,7 +178,6 @@ SystemSetting(
Setting('setting_limits', 'limit.problem_files_max', 100, 'number', 'limit.problem_files_max', 'Max files per problem'), 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', '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', '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.problem', 100, 'number', 'pagination.problem', 'Problems per page'),
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests 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'), Setting('setting_basic', 'pagination.discussion', 50, 'number', 'pagination.discussion', 'Discussions per page'),
@@ -187,6 +199,9 @@ SystemSetting(
Setting('setting_storage', 'installid', String.random(64), 'text', 'installid', 'Installation ID', FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'installid', String.random(64), 'text', 'installid', 'Installation ID', FLAG_HIDDEN | FLAG_DISABLED),
); );
// eslint-disable-next-line import/no-mutable-exports
export let langs = {};
bus.once('app/started', async () => { bus.once('app/started', async () => {
logger.debug('Ensuring settings'); logger.debug('Ensuring settings');
const system = global.Hydro.model.system; const system = global.Hydro.model.system;
@@ -199,13 +214,22 @@ bus.once('app/started', async () => {
} }
} }
try { try {
SETTINGS_BY_KEY['codeLang'].range = yaml.load(system.get('lang.texts')) as any; langs = parseLang(system.get('hydrooj.langs'));
global.Hydro.model.setting.langs = langs;
const range = {};
for (const key in langs) range[key] = langs[key].display;
LangSettingNode.range = range;
} catch (e) { /* Ignore */ } } catch (e) { /* Ignore */ }
}); });
bus.on('system/setting', (args) => { bus.on('system/setting', (args) => {
if (!args.lang?.texts) return; if (args.hydrooj?.langs) {
SETTINGS_BY_KEY['codeLang'].range = yaml.load(args.lang.texts) as any; langs = parseLang(args.hydrooj.langs);
global.Hydro.model.setting.langs = langs;
const range = {};
for (const key in langs) range[key] = langs[key].display;
LangSettingNode.range = range;
}
}); });
global.Hydro.model.setting = { global.Hydro.model.setting = {
@@ -228,4 +252,5 @@ global.Hydro.model.setting = {
DOMAIN_SETTINGS_BY_KEY, DOMAIN_SETTINGS_BY_KEY,
DOMAIN_USER_SETTINGS, DOMAIN_USER_SETTINGS,
DOMAIN_USER_SETTINGS_BY_KEY, DOMAIN_USER_SETTINGS_BY_KEY,
langs,
}; };
@@ -3,10 +3,12 @@ import delay from 'vj/utils/delay';
import CmEditor from '.'; import CmEditor from '.';
function runSubstitute($container) { function runSubstitute($container) {
const selector = ['textarea[data-markdown]']; $container.find('textarea[data-markdown]').get().forEach((element) => {
$container.find(selector.join(', ')).get().forEach((element) => {
CmEditor.getOrConstruct($(element)); CmEditor.getOrConstruct($(element));
}); });
$container.find('textarea[data-yaml]').get().forEach((element) => {
CmEditor.getOrConstruct($(element), { language: 'yaml' });
});
} }
const cmEditorPage = new AutoloadPage('cmEditorPage', () => { const cmEditorPage = new AutoloadPage('cmEditorPage', () => {
@@ -58,10 +58,16 @@ export default class Editor extends DOMAttachedObject {
lightbulb: { enabled: true }, lightbulb: { enabled: true },
model: this.model, model: this.model,
automaticLayout: true, automaticLayout: true,
wordWarp: 'bounded',
minimap: { enabled: false },
hideCursorInOverviewRuler: true,
overviewRulerLanes: 0,
overviewRulerBorder: false,
}; };
if (autoResize) { if (autoResize) {
cfg.scrollbar = { cfg.scrollbar = {
vertical: 'hidden', vertical: 'hidden',
horizontal: 'hidden',
handleMouseWheel: false, handleMouseWheel: false,
}; };
cfg.scrollBeyondLastLine = false; cfg.scrollBeyondLastLine = false;
@@ -4,7 +4,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
value: state.editor.code, value: state.editor.code,
language: window.MONACO_MODES[state.editor.lang], language: window.LANGS[state.editor.lang].monaco,
theme: 'vs-light', theme: 'vs-light',
mainSize: state.ui.main.size, mainSize: state.ui.main.size,
pretestSize: state.ui.pretest.size, pretestSize: state.ui.pretest.size,
@@ -136,8 +136,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
value={this.props.editorLang} value={this.props.editorLang}
onChange={(ev) => this.props.setEditorLanguage(ev.target.value)} onChange={(ev) => this.props.setEditorLanguage(ev.target.value)}
> >
{_.map(window.LANG_TEXTS, (val, key) => ( {_.map(window.LANGS, (val, key) => (
<option value={key} key={key}>{val}</option> <option value={key} key={key}>{val.display}</option>
))} ))}
</select> </select>
</ToolbarItem> </ToolbarItem>
+7 -21
View File
@@ -2,12 +2,13 @@
const { readdirSync, readFileSync } = require('fs'); const { readdirSync, readFileSync } = require('fs');
const { join } = require('path'); const { join } = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const yaml = require('js-yaml');
const { tmpdir } = require('os'); const { tmpdir } = require('os');
const bus = require('hydrooj/dist/service/bus'); const bus = require('hydrooj/dist/service/bus');
const markdown = require('./backendlib/markdown.js'); const markdown = require('./backendlib/markdown.js');
const { system, domain, user } = global.Hydro.model; const {
system, domain, user, setting,
} = global.Hydro.model;
const { Route, Handler, UiContextBase } = global.Hydro.service.server; const { Route, Handler, UiContextBase } = global.Hydro.service.server;
class WikiHelpHandler extends Handler { class WikiHelpHandler extends Handler {
@@ -17,20 +18,13 @@ class WikiHelpHandler extends Handler {
} }
async get() { async get() {
const [langstr, langtexts] = system.getMany(['hydrojudge.langs', 'lang.texts']); const LANGS = system.get('hydrooj.langs');
if (langstr) {
const languages = {}; const languages = {};
const LANGS = yaml.load(langstr);
const TEXTS = yaml.load(langtexts);
// eslint-disable-next-line guard-for-in // eslint-disable-next-line guard-for-in
for (const key in LANGS) { for (const key in LANGS) {
const name = TEXTS[key] || key; languages[LANGS[key].display] = LANGS[key].compile || LANGS[key].execute;
languages[name] = LANGS[key].type === 'compiler'
? LANGS[key].compile
: LANGS[key].execute;
} }
this.response.body = { languages }; this.response.body = { languages };
}
this.response.template = 'wiki_help.html'; this.response.template = 'wiki_help.html';
} }
} }
@@ -53,15 +47,7 @@ class UiConstantsHandler extends Handler {
} }
async get() { async get() {
const [LANG_TEXTS, LANG_HIGHLIGHT_ID, MONACO_MODES] = system.getMany([ this.response.body = `window.LANGS=${JSON.stringify(setting.langs)}`;
'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.type = 'text/javascript'; this.response.type = 'text/javascript';
this.ctx.set('nolog', '1'); this.ctx.set('nolog', '1');
} }
@@ -79,7 +65,7 @@ class UiSettingsHandler extends Handler {
nav_logo_dark, nav_logo_light, nav_logo_dark, nav_logo_light,
nav_logo_dark_2x, nav_logo_light_2x, nav_logo_dark_2x, nav_logo_light_2x,
header_background, header_background_2x, header_background, header_background_2x,
] = await system.getMany([ ] = system.getMany([
'ui-default.header_logo', 'ui-default.header_logo_2x', 'ui-default.header_logo', 'ui-default.header_logo_2x',
'ui-default.nav_logo_dark', 'ui-default.nav_logo_light', 'ui-default.nav_logo_dark', 'ui-default.nav_logo_light',
'ui-default.nav_logo_dark_2x', 'ui-default.nav_logo_light_2x', 'ui-default.nav_logo_dark_2x', 'ui-default.nav_logo_light_2x',
-1
View File
@@ -433,7 +433,6 @@ Memory: 内存
Message {0} not found.: 消息 {0} 不存在。 Message {0} not found.: 消息 {0} 不存在。
Messages: 消息 Messages: 消息
Method: 方法 Method: 方法
Monaco language modes: 编译语言代号 -> Monaco 编辑器模式
Month: 月 Month: 月
Monthly Popular: 月度最受欢迎 Monthly Popular: 月度最受欢迎
Most Upvoted Solutions: 最被赞同的题解 Most Upvoted Solutions: 最被赞同的题解
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.7.8", "version": "4.8.3",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -22,7 +22,7 @@
"@babel/preset-react": "^7.13.13", "@babel/preset-react": "^7.13.13",
"@babel/register": "^7.13.14", "@babel/register": "^7.13.14",
"@babel/runtime-corejs2": "^7.14.0", "@babel/runtime-corejs2": "^7.14.0",
"@hydrooj/utils": "^1.0.13", "@hydrooj/utils": "^1.0.15",
"ansi_up": "^5.0.1", "ansi_up": "^5.0.1",
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
@@ -2,6 +2,7 @@ import { NamedPage } from 'vj/misc/Page';
import load from 'vj/components/wastyle/index'; import load from 'vj/components/wastyle/index';
import code from 'vj/components/highlighter/code-example'; import code from 'vj/components/highlighter/code-example';
import i18n from 'vj/utils/i18n'; import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const page = new NamedPage('home_preference', async () => { const page = new NamedPage('home_preference', async () => {
async function mountComponent() { async function mountComponent() {
@@ -60,6 +61,66 @@ const page = new NamedPage('home_preference', async () => {
} }
} }
function setOptions($el, options) {
$el.empty();
$.each(options, (key, value) => {
$el.append($('<option></option>').attr('value', key).text(value));
});
}
const $el = $(tpl`\
<div class="row">
<div class="medium-5 columns form__item end">
<label>
${i18n('Code language')}
<div name="form_item_lang" class="select-container">
<select id="codelang-main-select" class="select"></select>
</div>
</label>
</div>
<div class="medium-5 columns form__item end" style="display: none" id="codelang-sub-container">
<label>
${i18n('Code language')}
<div name="form_item_lang" class="select-container">
<select id="codelang-sub-select" class="select"></select>
</div>
</label>
</div>
</div>
`);
$('[name="codeLang"]')
.parent().parent().parent()
.parent()
.hide()
.after($el);
$('#codelang-main-select').on('change', function () {
$('[name="codeLang"]').val(this.value);
const options = {};
for (const key in window.LANGS) {
if (key.startsWith(`${this.value}.`) && key !== this.value) options[key] = window.LANGS[key].display;
}
if (Object.keys(options).length > 1) {
setOptions($('#codelang-sub-select'), options);
$('#codelang-sub-container').show();
} else $('#codelang-sub-container').hide();
});
$('#codelang-sub-select').on('change', function () {
$('[name="codeLang"]').val(this.value);
});
const main = {};
for (const key in window.LANGS) {
if (!key.includes('.')) main[key] = window.LANGS[key].display;
}
setOptions($('#codelang-main-select'), main);
const current = $('[name="codeLang"]').val();
console.log(current);
if (current.includes('.')) {
const [m, s] = current.split('.');
$('#codelang-main-select').val(m);
$('#codelang-sub-select').val(s);
} else $('#codelang-main-select').val(current);
if (success) { if (success) {
render( render(
<AstylePreview></AstylePreview>, <AstylePreview></AstylePreview>,
@@ -1,10 +1,42 @@
import { NamedPage } from 'vj/misc/Page'; import { NamedPage } from 'vj/misc/Page';
function setOptions($el, options) {
$el.empty();
$.each(options, (key, value) => {
$el.append($('<option></option>').attr('value', key).text(value));
});
}
const page = new NamedPage('problem_submit', async () => { const page = new NamedPage('problem_submit', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => { $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide(); $(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show(); $('[name="problem-sidebar__categories"]').show();
}); });
$('#codelang-main-select').on('change', function () {
$('[name="lang"]').val(this.value);
const options = {};
for (const key in window.LANGS) {
if (key.startsWith(`${this.value}.`) && key !== this.value) options[key] = window.LANGS[key].display;
}
if (Object.keys(options).length > 1) {
setOptions($('#codelang-sub-select'), options);
$('#codelang-sub-container').show();
} else $('#codelang-sub-container').hide();
});
$('#codelang-sub-select').on('change', function () {
$('[name="lang"]').val(this.value);
});
const main = {};
for (const key in window.LANGS) {
if (!key.includes('.')) main[key] = window.LANGS[key].display;
}
setOptions($('#codelang-main-select'), main);
const current = $('[name="lang"]').val();
if (current.includes('.')) {
const [m, s] = current.split('.');
$('#codelang-main-select').val(m);
$('#codelang-sub-select').val(s);
} else $('#codelang-main-select').val(current);
}); });
export default page; export default page;
-38
View File
@@ -43,44 +43,6 @@ nav_logo_dark_2x:
name: nav_logo_dark_2x name: nav_logo_dark_2x
desc: Nav Logo Dark@2x desc: Nav Logo Dark@2x
default: /components/navigation/nav-logo-small@2x_dark.png 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: preferredEditorType:
type: type:
wysiwyg: WYSIWYG wysiwyg: WYSIWYG
@@ -50,6 +50,7 @@
{% set noAutocomplete = args.noAutocomplete or false %} {% set noAutocomplete = args.noAutocomplete or false %}
{% set extra_style = args.extra_style or false %} {% set extra_style = args.extra_style or false %}
{% set extra_class = args.extra_class %} {% set extra_class = args.extra_class %}
{% set extra_attr = args.extra_attr %}
{% set date = args.date or false %} {% set date = args.date or false %}
{% set time = args.time or false %} {% set time = args.time or false %}
class="{{ form_class }}{% if extra_class %} {{ extra_class }}{% endif %}" class="{{ form_class }}{% if extra_class %} {{ extra_class }}{% endif %}"
@@ -59,6 +60,7 @@
{% if noAutocomplete %}autocomplete="off"{% endif %} {% if noAutocomplete %}autocomplete="off"{% endif %}
{% if date %}data-pick-date{% endif %} {% if date %}data-pick-date{% endif %}
{% if time %}data-pick-time{% endif %} {% if time %}data-pick-time{% endif %}
{% if extra_attr %}{{ extra_attr }}{% endif %}
{% if extra_style %}style="{{extra_style}}"{% endif %} {% if extra_style %}style="{{extra_style}}"{% endif %}
{% endmacro %} {% endmacro %}
@@ -181,7 +183,7 @@
{% endmacro %} {% endmacro %}
{% macro form_textarea(args) %} {% macro form_textarea(args) %}
{{ form_begin({columns:8}|assign(args)) }} {{ form_begin({columns:10}|assign(args)) }}
{{ textarea(args) }} {{ textarea(args) }}
{{ form_end(args) }} {{ form_end(args) }}
{% endmacro %} {% endmacro %}
@@ -52,7 +52,8 @@
name:setting.key, name:setting.key,
value:'' if secret else (current[setting.key]|default(setting.value)), value:'' if secret else (current[setting.key]|default(setting.value)),
disabled:setting.disabled, disabled:setting.disabled,
placeholder:_('(Not changed)') if secret else '' placeholder:_('(Not changed)') if secret else '',
extra_attr: 'data-yaml' if setting.subType == 'yaml' else ''
}) }} }) }}
{% elif setting.type == 'markdown' %} {% elif setting.type == 'markdown' %}
{{ form.form_textarea({ {{ form.form_textarea({
@@ -42,12 +42,32 @@
</div> </div>
<div class="section__body"> <div class="section__body">
<form method="post"> <form method="post">
<div class="hasjs--hide">
{{ form.form_select({ {{ form.form_select({
label:'Code language', label:'Code language',
options:model.system.get('lang.texts')|parseYaml, options:model.setting.SETTINGS_BY_KEY['codeLang'].range,
name:'lang', name:'lang',
value:handler.user.codeLang value:handler.user.codeLang
}) }} }) }}
</div>
<div class="row nojs--hide">
<div class="medium-5 columns form__item end">
<label>
{{ _('Code language') }}
<div name="form_item_lang" class="select-container">
<select id="codelang-main-select" class="select"></select>
</div>
</label>
</div>
<div class="medium-5 columns form__item end" style="display: none" id="codelang-sub-container">
<label>
{{ _('Code language') }}
<div name="form_item_lang" class="select-container">
<select id="codelang-sub-select" class="select"></select>
</div>
</label>
</div>
</div>
{{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }} {{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
@@ -11,7 +11,7 @@
<h1 class="section__title">{{ _('Code') }}</h1> <h1 class="section__title">{{ _('Code') }}</h1>
</div> </div>
<div class="section__body"> <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>
</div> </div>
{% endif %} {% endif %}
@@ -63,7 +63,7 @@
{% endif %} {% endif %}
<dt>{{ _('Language') }}</dt> <dt>{{ _('Language') }}</dt>
<dd> <dd>
{{ (model.system.get('lang.texts')|parseYaml)[rdoc['lang']] }} {{ model.setting.langs[rdoc.lang].display }}
</dd> </dd>
<dt> <dt>
{{ _('Submit At') }} {{ _('Submit At') }}
@@ -30,6 +30,6 @@
</td> </td>
<td class="col--time">{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}&ge;{% endif %}{{ rdoc.time|round|int }}ms</td> <td class="col--time">{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}&ge;{% endif %}{{ rdoc.time|round|int }}ms</td>
<td class="col--memory">{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}&ge;{% endif %}{{ size(rdoc.memory, 1024) }}</td> <td class="col--memory">{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}&ge;{% endif %}{{ size(rdoc.memory, 1024) }}</td>
<td class="col--lang">{{ (model.system.get('lang.texts')|parseYaml)[rdoc['lang']] }}</td> <td class="col--lang">{{ model.setting.langs[rdoc['lang']].display }}</td>
<td class="col--submit-at">{{ datetimeSpan(rdoc['_id'])|safe }}</td> <td class="col--submit-at">{{ datetimeSpan(rdoc['_id'])|safe }}</td>
</tr> </tr>
+46
View File
@@ -0,0 +1,46 @@
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 === 'sub') continue;
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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/utils", "name": "@hydrooj/utils",
"version": "1.0.13", "version": "1.0.15",
"description": "hydrooj utils", "description": "hydrooj utils",
"main": "package.json", "main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",