judge: dedicate logger, add host vj4

This commit is contained in:
undefined committed 2020-10-27 00:06:21 +08:00
1 parent 6fa6ace318
commit 9a58c28a91
10 files changed
+758 -7

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
#!/bin/env node
require('./dist/daemon')()
+2 -1
View File
@@ -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": {
@@ -27,4 +28,4 @@
"build": "tsc",
"lint": "eslint . --ext ts --fix"
}
}
}
+61
View File
@@ -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;
+107
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
export { default as vj4, default } from './vj4';
+403
View File
@@ -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);
});
}
}
+142
View File
@@ -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');
+33
View File
@@ -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',
};
+5 -5
View File
@@ -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 = [];