mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 17:38:19 +08:00
judge: dedicate logger, add host vj4
This commit is contained in:
1 parent
6fa6ace318
commit
9a58c28a91
10 files changed
+757
-6
No files matched your search
+1
-1
Submodule packages/customize updated: 88b1f57a84...627cbb9437.
@@ -0,0 +1,3 @@
|
||||
#!/bin/env node
|
||||
|
||||
require('./dist/daemon')()
|
||||
@@ -11,6 +11,7 @@
|
||||
"lodash": "^4.17.20",
|
||||
"p-queue": "^6.6.1",
|
||||
"shell-quote": "^1.7.2",
|
||||
"supports-color": "^7.2.0",
|
||||
"systeminformation": "^4.27.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { argv } from 'yargs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import log from './log';
|
||||
|
||||
const config = {
|
||||
CONFIG_FILE: path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml'),
|
||||
LANGS_FILE: path.resolve(os.homedir(), '.config', 'hydro', 'langs.yaml'),
|
||||
CACHE_DIR: path.resolve(os.homedir(), '.cache', 'hydro', 'judge'),
|
||||
FILES_DIR: path.resolve(os.homedir(), '.cache', 'hydro', 'files', 'judge'),
|
||||
SYSTEM_MEMORY_LIMIT_MB: 1024,
|
||||
SYSTEM_TIME_LIMIT_MS: 16000,
|
||||
SYSTEM_PROCESS_LIMIT: 32,
|
||||
RETRY_DELAY_SEC: 15,
|
||||
TEMP_DIR: path.resolve(os.tmpdir(), 'hydro', 'judge'),
|
||||
EXECUTION_HOST: 'http://localhost:5050',
|
||||
CONFIG: null,
|
||||
LANGS: null,
|
||||
changeDefault(name, from, to) {
|
||||
if (config[name] === from) config[name] = to;
|
||||
},
|
||||
};
|
||||
|
||||
if (fs.existsSync(path.resolve(process.cwd(), '.env'))) {
|
||||
const env = {};
|
||||
const f = fs.readFileSync('.env').toString();
|
||||
for (const line of f) {
|
||||
const a = line.split('=');
|
||||
env[a[0]] = a[1];
|
||||
}
|
||||
Object.assign(process.env, env);
|
||||
}
|
||||
|
||||
if (process.env.CONFIG_FILE || argv.config) {
|
||||
config.CONFIG_FILE = path.resolve(process.env.CONFIG_FILE || argv.config as string);
|
||||
}
|
||||
if (process.env.LANGS_FILE || argv.langs) {
|
||||
config.LANGS_FILE = path.resolve(process.env.LANGS_FILE || argv.langs as string);
|
||||
}
|
||||
if (process.env.TEMP_DIR || argv.tmp) {
|
||||
config.TEMP_DIR = path.resolve(process.env.TEMP_DIR || argv.tmp as string);
|
||||
}
|
||||
if (process.env.CACHE_DIR || argv.cache) {
|
||||
config.CACHE_DIR = path.resolve(process.env.CACHE_DIR || argv.cache as string);
|
||||
}
|
||||
if (process.env.FILES_DIR || argv.files) {
|
||||
config.FILES_DIR = path.resolve(process.env.FILES_DIR || argv.files as string);
|
||||
}
|
||||
if (process.env.EXECUTION_HOST || argv.execute) {
|
||||
config.EXECUTION_HOST = path.resolve(process.env.EXECUTION_HOST || argv.execute as string);
|
||||
}
|
||||
if (!(fs.existsSync(config.LANGS_FILE) || global.Hydro)) {
|
||||
fs.ensureDirSync(path.dirname(config.LANGS_FILE));
|
||||
if (fs.existsSync(path.join(__dirname, '..', 'examples', 'langs.yaml'))) {
|
||||
log.error('Language file not found, using default.');
|
||||
config.LANGS_FILE = path.join(__dirname, '..', 'examples', 'langs.yaml');
|
||||
} else throw new Error('Language file not found');
|
||||
}
|
||||
|
||||
export = config;
|
||||
@@ -0,0 +1,107 @@
|
||||
/* eslint-disable no-constant-condition */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* ..
|
||||
.' @`._
|
||||
~ ...._.' ,__.-;
|
||||
_..- - - /` .-' ~
|
||||
: __./' , .'-'- .._
|
||||
~ `- -(.-'''- -. \`._ `. ~
|
||||
_.- '( .______.'.-' `-.` `.
|
||||
: `-..____`-. ;
|
||||
`. ```` 稻花香里说丰年, ; ~
|
||||
`-.__ 听取人生经验。 __.-'
|
||||
````- - -.......- - -''' ~
|
||||
~ */
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import * as Session from './hosts/index';
|
||||
import { sleep, Queue } from './utils';
|
||||
import log from './log';
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
onDestory: Function[]
|
||||
hosts: any
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!global.onDestory) global.onDestory = [];
|
||||
if (!global.hosts) global.hosts = [];
|
||||
let SI = false;
|
||||
|
||||
const { RETRY_DELAY_SEC, CONFIG_FILE, CONFIG } = require('./config');
|
||||
|
||||
const terminate = async () => {
|
||||
log.info('正在保存数据');
|
||||
try {
|
||||
await Promise.all(global.onDestory.map((f) => f()));
|
||||
process.exit(1);
|
||||
} catch (e) {
|
||||
if (SI) process.exit(1);
|
||||
log.error(e.stack);
|
||||
log.error('发生了错误。');
|
||||
log.error('再次按下 Ctrl-C 可强制退出。');
|
||||
SI = true;
|
||||
}
|
||||
};
|
||||
process.on('SIGINT', terminate);
|
||||
process.on('SIGTERM', terminate);
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise ', p);
|
||||
});
|
||||
|
||||
async function daemon() {
|
||||
let config;
|
||||
try {
|
||||
if (CONFIG) config = CONFIG;
|
||||
else {
|
||||
config = (await fs.readFile(CONFIG_FILE)).toString();
|
||||
config = yaml.safeLoad(config);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('配置文件无效或未找到。');
|
||||
process.exit(1);
|
||||
}
|
||||
const hosts = {};
|
||||
const queue = new Queue<any>();
|
||||
for (const i in config.hosts) {
|
||||
config.hosts[i].host = i;
|
||||
hosts[i] = new Session[config.hosts[i].type || 'vj4'](config.hosts[i]);
|
||||
await hosts[i].init();
|
||||
}
|
||||
global.hosts = hosts;
|
||||
if (!CONFIG) {
|
||||
global.onDestory.push(async () => {
|
||||
const cfg = { hosts: {} };
|
||||
for (const i in hosts) {
|
||||
cfg.hosts[i] = {
|
||||
host: i,
|
||||
type: hosts[i].config.type || 'default',
|
||||
uname: hosts[i].config.uname,
|
||||
password: hosts[i].config.password,
|
||||
server_url: hosts[i].config.server_url,
|
||||
};
|
||||
if (hosts[i].config.cookie) cfg.hosts[i].cookie = hosts[i].config.cookie;
|
||||
if (hosts[i].config.detail) cfg.hosts[i].detail = hosts[i].config.detail;
|
||||
}
|
||||
await fs.writeFile(CONFIG_FILE, yaml.safeDump(cfg));
|
||||
});
|
||||
}
|
||||
while ('Orz twd2') {
|
||||
try {
|
||||
for (const i in hosts) await hosts[i].consume(queue);
|
||||
while ('Orz iceb0y') {
|
||||
const [task] = await queue.get();
|
||||
task.handle();
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e, e.stack);
|
||||
log.info(`在 ${RETRY_DELAY_SEC} 秒后重试`);
|
||||
await sleep(RETRY_DELAY_SEC * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!module.parent) daemon();
|
||||
export = daemon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as vj4, default } from './vj4';
|
||||
@@ -0,0 +1,403 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import path from 'path';
|
||||
import child from 'child_process';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import WebSocket from 'ws';
|
||||
import * as tmpfs from '../tmpfs';
|
||||
import log from '../log';
|
||||
import { compilerText } from '../utils';
|
||||
import { CACHE_DIR, TEMP_DIR } 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;
|
||||
|
||||
next: (data: any, id?: number) => void;
|
||||
|
||||
end: (data: any) => void;
|
||||
|
||||
tmpdir: string;
|
||||
|
||||
clean: Function[];
|
||||
|
||||
folder: string;
|
||||
|
||||
config: any;
|
||||
|
||||
constructor(session, 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.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.next = this.getNext(this);
|
||||
this.end = this.getEnd(this.ws, this.tag);
|
||||
this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid);
|
||||
this.clean = [];
|
||||
fs.ensureDirSync(this.tmpdir);
|
||||
tmpfs.mount(this.tmpdir, '64m');
|
||||
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();
|
||||
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.bind(this),
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
getNext(that) { // eslint-disable-line class-methods-use-this
|
||||
that.nextId = 1;
|
||||
that.nextWaiting = [];
|
||||
return (data, id) => {
|
||||
data.key = 'next';
|
||||
data.tag = that.tag;
|
||||
if (id) {
|
||||
if (id === that.nextId) {
|
||||
that.ws.send(JSON.stringify(data));
|
||||
that.nextId++;
|
||||
let t = true;
|
||||
while (t) {
|
||||
t = false;
|
||||
for (const i in that.nextWaiting) {
|
||||
if (that.nextId === that.nextWaiting[i].id) {
|
||||
that.ws.send(JSON.stringify(that.nextWaiting[i].data));
|
||||
that.nextId++;
|
||||
that.nextWaiting.splice(i, 1);
|
||||
t = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else that.nextWaiting.push({ data, id });
|
||||
} else that.ws.send(JSON.stringify(data));
|
||||
};
|
||||
}
|
||||
|
||||
getEnd(ws, tag) { // eslint-disable-line class-methods-use-this
|
||||
return (data) => {
|
||||
data.key = 'end';
|
||||
data.tag = tag;
|
||||
log.info('Status=%d Score=%s Time=%dms Memory=%dkb', data.status, data.score, data.time_ms, data.memory_kb);
|
||||
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;
|
||||
let err;
|
||||
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, 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(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 = await 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));
|
||||
await new Promise((resolve, reject) => {
|
||||
child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => {
|
||||
if (e) reject(e);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
await fs.unlink(tmpFilePath);
|
||||
await this.processData(savePath).catch();
|
||||
} catch (e) {
|
||||
if (retry) await this.problemData(domainId, pid, savePath, retry - 1);
|
||||
else throw e;
|
||||
}
|
||||
return savePath;
|
||||
}
|
||||
|
||||
async recordPretestData(rid, savePath) {
|
||||
log.info(`Getting pretest data: ${this.config.host}/${rid}`);
|
||||
const tmpFilePath = path.resolve(CACHE_DIR, `download_${this.config.host}_${rid}`);
|
||||
await this.ensureLogin();
|
||||
const res = await this.axios.get(`records/${rid}/data`, { responseType: 'stream' });
|
||||
const w = await 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));
|
||||
await new Promise((resolve, reject) => {
|
||||
child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => {
|
||||
if (e) reject(e);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
await fs.unlink(tmpFilePath);
|
||||
await this.processData(savePath);
|
||||
return savePath;
|
||||
}
|
||||
|
||||
async consume(queue) {
|
||||
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(); });
|
||||
});
|
||||
log.info(`[${this.config.host}] 已连接`);
|
||||
}
|
||||
|
||||
async setCookie(cookie) {
|
||||
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) { // 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) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
|
||||
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(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) return filePath;
|
||||
fs.removeSync(filePath);
|
||||
}
|
||||
fs.ensureDirSync(domainDir);
|
||||
await this.problemData(domainId, pid, filePath, 3, next);
|
||||
fs.writeFileSync(path.join(filePath, 'version'), version);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async retry(queue) {
|
||||
this.consume(queue).catch(() => {
|
||||
setTimeout(() => {
|
||||
this.retry(queue);
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { inspect, InspectOptions, format } from 'util';
|
||||
import { argv } from 'yargs';
|
||||
import { stderr } from 'supports-color';
|
||||
|
||||
namespace Time {
|
||||
export const second = 1000;
|
||||
export const minute = second * 60;
|
||||
export const hour = minute * 60;
|
||||
export const day = hour * 24;
|
||||
export const week = day * 7;
|
||||
export function formatTimeShort(ms: number) {
|
||||
const abs = Math.abs(ms);
|
||||
if (abs >= day - hour / 2) {
|
||||
return `${Math.round(ms / day)}d`;
|
||||
} if (abs >= hour - minute / 2) {
|
||||
return `${Math.round(ms / hour)}h`;
|
||||
} if (abs >= minute - second / 2) {
|
||||
return `${Math.round(ms / minute)}m`;
|
||||
} if (abs >= second) {
|
||||
return `${Math.round(ms / second)}s`;
|
||||
}
|
||||
return `${ms}ms`;
|
||||
}
|
||||
}
|
||||
|
||||
const colors = stderr.level < 2 ? [6, 2, 3, 4, 5, 1] : [
|
||||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
||||
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
||||
129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
|
||||
169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
|
||||
201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
|
||||
];
|
||||
|
||||
const instances: Record<string, Logger> = {};
|
||||
|
||||
type LogFunction = (format: any, ...param: any[]) => void
|
||||
|
||||
type LogType = 'success' | 'error' | 'info' | 'warn' | 'debug'
|
||||
|
||||
export interface Logger extends Record<LogType, LogFunction> { }
|
||||
|
||||
export class Logger {
|
||||
static readonly SUCCESS = 1
|
||||
|
||||
static readonly ERROR = 1
|
||||
|
||||
static readonly INFO = 2
|
||||
|
||||
static readonly WARN = 2
|
||||
|
||||
static readonly DEBUG = 3
|
||||
|
||||
static baseLevel = argv.debug ? 3 : 2
|
||||
|
||||
static showDiff = false
|
||||
|
||||
static levels: Record<string, number> = {}
|
||||
|
||||
static lastTime = 0
|
||||
|
||||
static options: InspectOptions = {
|
||||
colors: stderr.hasBasic,
|
||||
}
|
||||
|
||||
static formatters: Record<string, (this: Logger, value: any) => string> = {
|
||||
c: Logger.prototype.color,
|
||||
C: (value) => Logger.color(15, value, ';1'),
|
||||
o: (value) => inspect(value, Logger.options).replace(/\s*\n\s*/g, ' '),
|
||||
}
|
||||
|
||||
static color(code: number, value: any, decoration = '') {
|
||||
if (!Logger.options.colors) return `${value}`;
|
||||
return `\u001B[3${code < 8 ? code : `8;5;${code}`}${decoration}m${value}\u001B[0m`;
|
||||
}
|
||||
|
||||
private code: number
|
||||
|
||||
private displayName: string
|
||||
|
||||
public stream: NodeJS.WritableStream = process.stderr
|
||||
|
||||
constructor(public name: string, private showDiff = false) {
|
||||
if (name in instances) return instances[name];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = ((hash << 3) - hash) + name.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
instances[name] = this;
|
||||
this.code = colors[Math.abs(hash) % colors.length];
|
||||
this.displayName = name ? this.color(`${name} `, ';1') : '';
|
||||
this.createMethod('success', '[S] ', Logger.SUCCESS);
|
||||
this.createMethod('error', '[E] ', Logger.ERROR);
|
||||
this.createMethod('info', '[I] ', Logger.INFO);
|
||||
this.createMethod('warn', '[W] ', Logger.WARN);
|
||||
this.createMethod('debug', '[D] ', Logger.DEBUG);
|
||||
}
|
||||
|
||||
private color(value: any, decoration = '') {
|
||||
return Logger.color(this.code, value, decoration);
|
||||
}
|
||||
|
||||
private createMethod(name: LogType, prefix: string, minLevel: number) {
|
||||
this[name] = (...args: [any, ...any[]]) => {
|
||||
if (this.level < minLevel) return;
|
||||
this.stream.write(`${prefix + this.displayName + this.format(...args)}\n`);
|
||||
};
|
||||
}
|
||||
|
||||
get level() {
|
||||
return Logger.levels[this.name] ?? Logger.baseLevel;
|
||||
}
|
||||
|
||||
extend = (namespace: string, showDiff = this.showDiff) => new Logger(`${this.name}:${namespace}`, showDiff)
|
||||
|
||||
format: (format: any, ...param: any[]) => string = (...args) => {
|
||||
if (args[0] instanceof Error) args[0] = args[0].stack || args[0].message;
|
||||
else if (typeof args[0] !== 'string') args.unshift('%O');
|
||||
let index = 0;
|
||||
args[0] = (args[0] as string).replace(/%([a-zA-Z%])/g, (match, fmt) => {
|
||||
if (match === '%%') return '%';
|
||||
index += 1;
|
||||
const formatter = Logger.formatters[fmt];
|
||||
if (typeof formatter === 'function') {
|
||||
match = formatter.call(this, args[index]);
|
||||
args.splice(index, 1);
|
||||
index -= 1;
|
||||
}
|
||||
return match;
|
||||
}).split('\n').join('\n ');
|
||||
if (Logger.showDiff || this.showDiff) {
|
||||
const now = Date.now();
|
||||
if (Logger.lastTime) {
|
||||
args.push(this.color(`+${Time.formatTimeShort(now - Logger.lastTime)}`));
|
||||
}
|
||||
Logger.lastTime = now;
|
||||
}
|
||||
return format(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger('judge');
|
||||
@@ -0,0 +1,33 @@
|
||||
export const STATUS_WAITING = 0;
|
||||
export const STATUS_ACCEPTED = 1;
|
||||
export const STATUS_WRONG_ANSWER = 2;
|
||||
export const STATUS_TIME_LIMIT_EXCEEDED = 3;
|
||||
export const STATUS_MEMORY_LIMIT_EXCEEDED = 4;
|
||||
export const STATUS_OUTPUT_LIMIT_EXCEEDED = 5;
|
||||
export const STATUS_RUNTIME_ERROR = 6;
|
||||
export const STATUS_COMPILE_ERROR = 7;
|
||||
export const STATUS_SYSTEM_ERROR = 8;
|
||||
export const STATUS_CANCELED = 9;
|
||||
export const STATUS_ETC = 10;
|
||||
export const STATUS_JUDGING = 20;
|
||||
export const STATUS_COMPILING = 21;
|
||||
export const STATUS_FETCHED = 22;
|
||||
export const STATUS_IGNORED = 30;
|
||||
|
||||
export const STATUS_TEXTS = {
|
||||
[STATUS_WAITING]: 'Waiting',
|
||||
[STATUS_ACCEPTED]: 'Accepted',
|
||||
[STATUS_WRONG_ANSWER]: 'Wrong Answer',
|
||||
[STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded',
|
||||
[STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded',
|
||||
[STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded',
|
||||
[STATUS_RUNTIME_ERROR]: 'Runtime Error',
|
||||
[STATUS_COMPILE_ERROR]: 'Compile Error',
|
||||
[STATUS_SYSTEM_ERROR]: 'System Error',
|
||||
[STATUS_CANCELED]: 'Cancelled',
|
||||
[STATUS_ETC]: 'Unknown Error',
|
||||
[STATUS_JUDGING]: 'Running',
|
||||
[STATUS_COMPILING]: 'Compiling',
|
||||
[STATUS_FETCHED]: 'Fetched',
|
||||
[STATUS_IGNORED]: 'Ignored',
|
||||
};
|
||||
@@ -38,8 +38,8 @@ export function parseFilename(filePath: string) {
|
||||
return t[t.length - 1];
|
||||
}
|
||||
|
||||
export class Queue extends EventEmitter {
|
||||
queue: any[];
|
||||
export class Queue<T> extends EventEmitter {
|
||||
queue: T[];
|
||||
|
||||
waiting: any[];
|
||||
|
||||
@@ -51,21 +51,21 @@ export class Queue extends EventEmitter {
|
||||
|
||||
get(count = 1) {
|
||||
if (this.empty() || this.queue.length < count) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<T[]>((resolve) => {
|
||||
this.waiting.push({ count, resolve });
|
||||
});
|
||||
}
|
||||
const items = [];
|
||||
for (let i = 0; i < count; i++) { items.push(this.queue[i]); }
|
||||
this.queue = _.drop(this.queue, count);
|
||||
return items;
|
||||
return items as T[];
|
||||
}
|
||||
|
||||
empty() {
|
||||
return this.queue.length === 0;
|
||||
}
|
||||
|
||||
push(value) {
|
||||
push(value: T) {
|
||||
this.queue.push(value);
|
||||
if (this.waiting.length && this.waiting[0].count <= this.queue.length) {
|
||||
const items = [];
|
||||
|
||||
Reference in new issue
Block a user