From 9a58c28a917c447634f124c85ffad58d531164b5 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 27 Oct 2020 00:06:21 +0800 Subject: [PATCH] judge: dedicate logger, add host vj4 --- packages/customize | 2 +- packages/hydrojudge/bin/hydrojudge.js | 3 + packages/hydrojudge/package.json | 3 +- packages/hydrojudge/src/config.ts | 61 ++++ packages/hydrojudge/src/daemon.ts | 107 +++++++ packages/hydrojudge/src/hosts/index.ts | 1 + packages/hydrojudge/src/hosts/vj4.ts | 403 +++++++++++++++++++++++++ packages/hydrojudge/src/log.ts | 142 +++++++++ packages/hydrojudge/src/status.ts | 33 ++ packages/hydrojudge/src/utils.ts | 10 +- 10 files changed, 758 insertions(+), 7 deletions(-) create mode 100644 packages/hydrojudge/bin/hydrojudge.js create mode 100644 packages/hydrojudge/src/config.ts create mode 100644 packages/hydrojudge/src/daemon.ts create mode 100644 packages/hydrojudge/src/hosts/index.ts create mode 100644 packages/hydrojudge/src/hosts/vj4.ts create mode 100644 packages/hydrojudge/src/log.ts create mode 100644 packages/hydrojudge/src/status.ts diff --git a/packages/customize b/packages/customize index 88b1f57a8..627cbb943 160000 --- a/packages/customize +++ b/packages/customize @@ -1 +1 @@ -Subproject commit 88b1f57a84f4db58489fec74897cdd961d35c1a5 +Subproject commit 627cbb9437268b82a74c6fabb85e3480ec6ad1fc diff --git a/packages/hydrojudge/bin/hydrojudge.js b/packages/hydrojudge/bin/hydrojudge.js new file mode 100644 index 000000000..1d7052b5e --- /dev/null +++ b/packages/hydrojudge/bin/hydrojudge.js @@ -0,0 +1,3 @@ +#!/bin/env node + +require('./dist/daemon')() \ No newline at end of file diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index ce3b58e29..613e9fb79 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/hydrojudge/src/config.ts b/packages/hydrojudge/src/config.ts new file mode 100644 index 000000000..7a108f73a --- /dev/null +++ b/packages/hydrojudge/src/config.ts @@ -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; diff --git a/packages/hydrojudge/src/daemon.ts b/packages/hydrojudge/src/daemon.ts new file mode 100644 index 000000000..3af8f2b8c --- /dev/null +++ b/packages/hydrojudge/src/daemon.ts @@ -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(); + 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; diff --git a/packages/hydrojudge/src/hosts/index.ts b/packages/hydrojudge/src/hosts/index.ts new file mode 100644 index 000000000..471312a3d --- /dev/null +++ b/packages/hydrojudge/src/hosts/index.ts @@ -0,0 +1 @@ +export { default as vj4, default } from './vj4'; diff --git a/packages/hydrojudge/src/hosts/vj4.ts b/packages/hydrojudge/src/hosts/vj4.ts new file mode 100644 index 000000000..896a082b9 --- /dev/null +++ b/packages/hydrojudge/src/hosts/vj4.ts @@ -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; + + 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); + }); + } +} diff --git a/packages/hydrojudge/src/log.ts b/packages/hydrojudge/src/log.ts new file mode 100644 index 000000000..63ebefb73 --- /dev/null +++ b/packages/hydrojudge/src/log.ts @@ -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 = {}; + +type LogFunction = (format: any, ...param: any[]) => void + +type LogType = 'success' | 'error' | 'info' | 'warn' | 'debug' + +export interface Logger extends Record { } + +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 = {} + + static lastTime = 0 + + static options: InspectOptions = { + colors: stderr.hasBasic, + } + + static formatters: Record 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'); diff --git a/packages/hydrojudge/src/status.ts b/packages/hydrojudge/src/status.ts new file mode 100644 index 000000000..56a955aa5 --- /dev/null +++ b/packages/hydrojudge/src/status.ts @@ -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', +}; diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts index 45d647aed..f18d5d397 100644 --- a/packages/hydrojudge/src/utils.ts +++ b/packages/hydrojudge/src/utils.ts @@ -38,8 +38,8 @@ export function parseFilename(filePath: string) { return t[t.length - 1]; } -export class Queue extends EventEmitter { - queue: any[]; +export class Queue 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((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 = [];