diff --git a/build/build.js b/build/build.js index 912f93af2..401714b94 100644 --- a/build/build.js +++ b/build/build.js @@ -52,7 +52,7 @@ for (const package of packages) { if (package === 'ui-default') continue; const basedir = path.resolve(process.cwd(), 'packages', package); const files = fs.readdirSync(basedir); - if (!files.includes('src') && !files.map(n => n.split('.')[1]).includes('ts')) continue; + if (!files.includes('src') && !files.map(n => n.split('.')[1]).includes('ts') && package !== 'utils') continue; if (package !== 'hydrooj') config.references.push({ path: `packages/${package}` }); fs.writeFileSync( path.resolve(basedir, 'tsconfig.json'), diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 1007a5976..eb02d7073 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.4.17", + "version": "2.4.18", "os": [ "linux" ], @@ -9,6 +9,7 @@ "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", "dependencies": { + "@hydrooj/utils": "^1.0.0", "adm-zip": "^0.5.5", "axios": "^0.21.1", "bson": "^4.2.3", @@ -17,7 +18,6 @@ "lodash": "^4.17.21", "p-queue": "^6.6.1", "shell-quote": "^1.7.2", - "systeminformation": "5.6.10", "ws": "^7.4.4", "yargs": "^16.2.0" }, diff --git a/packages/hydrojudge/src/hosts/vj4.ts b/packages/hydrojudge/src/hosts/vj4.ts index 7d1943cd5..6d8333606 100644 --- a/packages/hydrojudge/src/hosts/vj4.ts +++ b/packages/hydrojudge/src/hosts/vj4.ts @@ -14,19 +14,6 @@ import { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } from '../status'; import readCases from '../cases'; import judge from '../judge'; -function formatStr(content: string, ...args) { - let result = content; - if (args.length > 0) { - for (const key in args) { - if (args[key] !== undefined) { - const reg = new RegExp(`(\\{${key}\\})`, 'g'); - result = result.replace(reg, args[key]); - } - } - } - return result; -} - class JudgeTask { stat: Record; session: any; @@ -131,14 +118,14 @@ class JudgeTask { delete data.message; } if (data.judge_text && typeof data.judge_text !== 'string') { - data.judge_text = formatStr(data.judge_text.message, ...data.judge_text.params); + 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 = formatStr(data.case.judge_text.message, ...data.case.judge_text.params); + data.case.judge_text = data.case.judge_text.message.format(data.case.judge_text.params); } data.key = 'next'; data.tag = this.tag; diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts index e06372b56..9253939b4 100644 --- a/packages/hydrojudge/src/service.ts +++ b/packages/hydrojudge/src/service.ts @@ -8,6 +8,7 @@ import fs from 'fs-extra'; import { noop } from 'lodash'; import { Logger } from 'hydrooj/dist/logger'; import * as monitor from 'hydrooj/dist/service/monitor'; +import * as sysinfo from './sysinfo'; import * as tmpfs from './tmpfs'; import { FormatError, CompileError, SystemError } from './error'; import { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } from './status'; @@ -36,7 +37,6 @@ async function postInit() { // Only start a single daemon if (!global.Hydro.isFirstWorker) return; const judge = require('./judge'); - const sysinfo = require('./sysinfo'); const { task, system } = global.Hydro.model; const { storage } = global.Hydro.service; diff --git a/packages/hydrojudge/src/sysinfo.ts b/packages/hydrojudge/src/sysinfo.ts index 3355bacc1..99290edd9 100644 --- a/packages/hydrojudge/src/sysinfo.ts +++ b/packages/hydrojudge/src/sysinfo.ts @@ -1,23 +1,12 @@ import path from 'path'; import fs from 'fs-extra'; -import systeminformation from 'systeminformation'; import { noop } from 'lodash'; +import { get as _get } from '@hydrooj/utils/lib/sysinfo'; import { judge } from './judge/run'; import * as tmpfs from './tmpfs'; import { getConfig } from './config'; -function size(s: number, base = 1) { - s *= base; - const unit = 1024; - const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; - for (const unitName of unitNames) { - if (s < unit) return `${Math.round(s * 10) / 10} ${unitName}`; - s /= unit; - } - return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`; -} - -const cache: any = {}; +export { update } from '@hydrooj/utils/lib/sysinfo'; async function stackSize() { let output = ''; @@ -35,8 +24,8 @@ int main(){ main(); }`, config: { - time: 3000, - memory: 256, + time: '3s', + memory: '256m', }, stat: {}, clean: [], @@ -61,38 +50,10 @@ int main(){ } export async function get() { - const [ - Cpu, Memory, OsInfo, - CurrentLoad, CpuFlags, CpuTemp, - Battery, stack, - ] = await Promise.all([ - systeminformation.cpu(), - systeminformation.mem(), - systeminformation.osInfo(), - systeminformation.currentLoad(), - systeminformation.cpuFlags(), - systeminformation.cpuTemperature(), - systeminformation.battery(), - stackSize(), - ]); - const cpu = `${Cpu.manufacturer} ${Cpu.brand}`; - const memory = `${size(Memory.active)}/${size(Memory.total)}`; - const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`; - const load = `${CurrentLoad.avgLoad}`; - const flags = CpuFlags; - let battery; - if (!Battery.hasBattery) battery = 'No battery'; - else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.isCharging ? ' Charging' : ''}`; - const mid = OsInfo.serial; - cache.cpu = cpu; - cache.osinfo = osinfo; - cache.flags = flags; - cache.mid = mid; - cache.stack = stack; - global.reqCount = 0; - return { - mid, cpu, memory, osinfo, load, flags, CpuTemp, battery, stack, reqCount: 0, - }; + const info = await _get(); + // @ts-ignore + info.stack = await stackSize(); + return info; } declare global { @@ -102,31 +63,3 @@ declare global { } } } - -export async function update() { - const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([ - systeminformation.mem(), - systeminformation.currentLoad(), - systeminformation.cpuTemperature(), - systeminformation.battery(), - ]); - const { - mid, cpu, osinfo, flags, stack, - } = cache; - const memory = `${size(Memory.active)}/${size(Memory.total)}`; - const load = `${CurrentLoad.avgLoad}`; - let battery; - if (!Battery.hasBattery) battery = 'No battery'; - else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.isCharging ? ' Charging' : ''}`; - const reqCount = global.reqCount; - global.reqCount = 0; - return [ - mid, - { - memory, load, battery, CpuTemp, reqCount, - }, - { - mid, cpu, memory, osinfo, load, flags, battery, CpuTemp, stack, reqCount, - }, - ]; -} diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts index e43f04a8d..a8ab7f27f 100644 --- a/packages/hydrojudge/src/utils.ts +++ b/packages/hydrojudge/src/utils.ts @@ -5,34 +5,9 @@ import _ from 'lodash'; import { EventEmitter } from 'events'; import { FormatError } from './error'; -const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; -const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; -const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; -const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 }; const EMPTY_STR = /^[ \r\n\t]*$/i; export const cmd = parse; -export function noop() { } - -export function parseTimeMS(val: string | number) { - if (typeof val === 'number') return val; - const match = TIME_RE.exec(val); - if (!match) throw new FormatError('Error parsing time: {0}', [val]); - return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); -} - -export function parseMemoryMB(val: string | number) { - if (typeof val === 'number') return val; - const match = MEMORY_RE.exec(val); - if (!match) throw new FormatError('Error parsing memory: {0}', [val]); - return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); -} - -export function sleep(timeout: number) { - return new Promise((resolve) => { - setTimeout(resolve, timeout); - }); -} export function parseFilename(filePath: string) { const t = filePath.split('/'); @@ -112,3 +87,5 @@ export function ensureFile(folder: string) { return f; }; } + +export * from '@hydrooj/utils/lib/utils'; diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 69abaf9e1..3f020250f 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.21.1", + "version": "2.21.2", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", @@ -11,6 +11,7 @@ "node": ">=14" }, "dependencies": { + "@hydrooj/utils": "^1.0.0", "adm-zip": "^0.5.5", "ansi_up": "^5.0.0", "cookies": "^0.8.0", @@ -34,7 +35,6 @@ "serialize-javascript": "^5.0.1", "sockjs": "^0.3.20", "superagent": "^6.1.0", - "systeminformation": "5.6.10", "yargs": "^16.2.0" }, "devDependencies": { diff --git a/packages/hydrooj/src/entry/worker.ts b/packages/hydrooj/src/entry/worker.ts index ca43efe28..6b690c38d 100644 --- a/packages/hydrooj/src/entry/worker.ts +++ b/packages/hydrooj/src/entry/worker.ts @@ -44,7 +44,15 @@ export async function load() { const storage = require('../service/storage'); storage.start(sopts); if (argv.loaderDetail) logger.info('finish: storage.connect'); - for (const i of builtinLib) require(`../lib/${i}`); + for (const i of builtinLib) { + let t; + try { + t = require.resolve(`../lib/${i}`); + } catch (e) { + t = require.resolve(`@hydrooj/utils/lib/${i}`); + } + require(t); + } if (argv.loaderDetail) logger.info('finish: lib.builtin'); await lib(pending, fail); if (argv.loaderDetail) logger.info('finish: lib.extra'); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index fe18dd912..a32f66294 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -540,7 +540,6 @@ export interface Lib extends Record { rank: typeof import('./lib/rank'), rating: typeof import('./lib/rating'), testdataConfig: typeof import('./lib/testdataConfig'), - sysinfo: typeof import('./lib/sysinfo'), 'testdata.convert.ini': typeof import('./lib/testdata.convert.ini'), useragent: typeof import('./lib/useragent'), validator: typeof import('./lib/validator'), diff --git a/packages/hydrooj/src/lib/i18n.ts b/packages/hydrooj/src/lib/i18n.ts index 946522908..1446c6429 100644 --- a/packages/hydrooj/src/lib/i18n.ts +++ b/packages/hydrooj/src/lib/i18n.ts @@ -2,44 +2,10 @@ const locales = {}; declare global { interface String { - format: (...args: Array) => string; - formatFromArray: (args: any[]) => string; - rawformat: (object: any) => string; translate: (...languages: string[]) => string; } } -String.prototype.format = function formatStr(...args) { - let result = this; - if (args.length > 0) { - if (args.length === 1 && typeof (args[0]) === 'object') { - for (const key in args) { - if (args[key] !== undefined) { - const reg = new RegExp(`(\\{${key}\\})`, 'g'); - result = result.replace(reg, args[key]); - } - } - } else return this.formatFromArray(args); - } - return result; -}; - -String.prototype.formatFromArray = function formatStr(args) { - let result = this; - for (let i = 0; i < args.length; i++) { - if (args[i] !== undefined) { - const reg = new RegExp(`(\\{)${i}(\\})`, 'g'); - result = result.replace(reg, args[i]); - } - } - return result; -}; - -String.prototype.rawformat = function rawFormat(object) { - const res = this.split('{@}'); - return [res[0], object, res[1]].join(); -}; - String.prototype.translate = function translate(...languages: string[]) { for (const language of languages) { if (!language) continue; diff --git a/packages/hydrooj/src/lib/misc.ts b/packages/hydrooj/src/lib/misc.ts index 8e15338df..7ebb51964 100644 --- a/packages/hydrooj/src/lib/misc.ts +++ b/packages/hydrooj/src/lib/misc.ts @@ -1,7 +1,10 @@ import AnsiUp from 'ansi_up'; import { ObjectID } from 'mongodb'; +import { size, formatSeconds } from '@hydrooj/utils/lib/utils'; import { md5 } from './crypto'; +export { size, formatSeconds } from '@hydrooj/utils/lib/utils'; + const AU = new AnsiUp(); export function ansiToHtml(str: string) { @@ -49,30 +52,6 @@ export function* paginate(page: number, numPages: number) { } } -export function size(s: number, base = 1) { - s *= base; - const unit = 1024; - const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; - for (const unitName of unitNames) { - if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName); - s /= unit; - } - return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`; -} - -function _digit2(number: number) { - return number < 10 ? `0${number}` : number.toString(); -} - -export function formatSeconds(_seconds = '0') { - const seconds = parseInt(_seconds, 10); - return '{0}:{1}:{2}'.format( - _digit2(Math.floor(seconds / 3600)), - _digit2(Math.floor((seconds % 3600) / 60)), - _digit2(seconds % 60), - ); -} - global.Hydro.lib.misc = { gravatar, datetimeSpan, paginate, size, formatSeconds, ansiToHtml, }; diff --git a/packages/hydrooj/src/service/monitor.ts b/packages/hydrooj/src/service/monitor.ts index 35d71b332..2202b899a 100644 --- a/packages/hydrooj/src/service/monitor.ts +++ b/packages/hydrooj/src/service/monitor.ts @@ -2,11 +2,11 @@ import cluster from 'cluster'; import crypto from 'crypto'; import superagent from 'superagent'; import { dump } from 'js-yaml'; +import * as sysinfo from '@hydrooj/utils/lib/sysinfo'; +import type { StatusUpdate } from '@hydrooj/utils/lib/sysinfo'; import db from './db'; import * as bus from './bus'; -import * as sysinfo from '../lib/sysinfo'; import { Logger } from '../logger'; -import type { StatusUpdate } from '../lib/sysinfo'; const coll = db.collection('status'); const logger = new Logger('monitor'); diff --git a/packages/hydrooj/src/utils.ts b/packages/hydrooj/src/utils.ts index 985cbed5b..81f587467 100644 --- a/packages/hydrooj/src/utils.ts +++ b/packages/hydrooj/src/utils.ts @@ -1,246 +1,17 @@ -import fs from 'fs'; -import { Duplex } from 'stream'; import cluster from 'cluster'; -import path, { sep } from 'path'; -import { ObjectID } from 'mongodb'; -import { isMoment } from 'moment'; -import type { Moment } from 'moment-timezone'; import type { Logger } from './logger'; -declare global { - interface StringConstructor { - random: (digit?: number) => string; - } - interface ArrayConstructor { - isDiff: (a: any[], b: any[]) => boolean; - } - interface Date { - format: (fmt?: string) => string; - } - interface Math { - sum: (...args: Array) => number; - } - interface SetConstructor { - isSuperset: (set: Set, subset: Set) => boolean; - intersection: (setA: Set, setB: Set) => Set; - union: (setA: Set, setB: Set) => Set; - } -} - if (!cluster.worker) { // @ts-ignore cluster.worker = { id: 0 }; } -const dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; - -String.random = function random(digit = 32) { - let str = ''; - for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * 62)]; - return str; -}; - -Array.isDiff = function isDiff(a, b) { - if (a.length !== b.length) return true; - a.sort(); - b.sort(); - for (const i in a) { - if (a[i] !== b[i]) return true; - } - return false; -}; - -Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { - let m = this.getMonth() + 1; - if (m < 10) m = `0${m}`; - let d = this.getDate(); - if (d < 10) d = `0${d}`; - let H = this.getHours(); - if (H < 10) H = `0${H}`; - let M = this.getMinutes(); - if (M < 10) M = `0${M}`; - let S = this.getSeconds(); - if (S < 10) S = `0${S}`; - return fmt - .replace('%Y', this.getFullYear()) - .replace('%m', m) - .replace('%d', d) - .replace('%H', H) - .replace('%M', M) - .replace('%S', S); -}; - -Math.sum = function sum(...args) { - let s = 0; - for (const i of args) { - if (i instanceof Array) { - for (const j of i) { - s += j; - } - } else s += i; - } - return s; -}; - -Set.isSuperset = function isSuperset(set, subset) { - for (const elem of subset) { - if (!set.has(elem)) return false; - } - return true; -}; - -Set.union = function Union(setA: Set, setB: Set) { - const union = new Set(setA); - for (const elem of setB) union.add(elem); - return union; -}; - -Set.intersection = function Intersection(setA: Set, setB: Set): Set { - const intersection = new Set(); - for (const elem of setB) { - if (setA.has(elem)) intersection.add(elem); - } - // @ts-ignore - return intersection; -}; - -export function folderSize(folderPath: string) { - let size = 0; - const _next = function a(p: string) { - if (p) { - const stats = fs.statSync(p); - if (!stats.isDirectory() || stats.isSymbolicLink()) { - if (!stats.isSymbolicLink()) size += stats.size; - } else { - size += stats.size; - const files = fs.readdirSync(p); - if (Array.isArray(files)) { - files.forEach((file) => { - _next(path.join(p, file)); - }); - } - } - } - }; - _next(folderPath); - return size; -} - -const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; -const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; -const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; -const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 }; - -export function parseTimeMS(str: string) { - const match = TIME_RE.exec(str); - if (!match) throw new Error(`${str} error parsing time`); - return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); -} - -export function parseMemoryMB(str: string) { - const match = MEMORY_RE.exec(str); - if (!match) throw new Error(`${str} error parsing memory`); - return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); -} - -export function isClass(obj: any, strict = false) { - if (typeof obj !== 'function') return false; - const str = obj.toString(); - if (obj.prototype === undefined) return false; - if (obj.prototype.constructor !== obj) return false; - if (str.slice(0, 5) === 'class') return true; - if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true; - if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false; - if (strict && /^function\s+[A-Z]/.test(str)) return true; - if (/\b\(this\b|\bthis[.[]\b/.test(str)) { - if (!strict || /classCallCheck\(this/.test(str)) return true; - return /^function\sdefault_\d+\s*\(/.test(str); - } - return false; -} - -export function streamToBuffer(stream): Promise { - return new Promise((resolve, reject) => { - const buffers = []; - stream.on('error', reject); - stream.on('data', (data) => buffers.push(data)); - stream.on('end', () => resolve(Buffer.concat(buffers))); - }); -} - -export function bufferToStream(buffer: Buffer): NodeJS.ReadableStream { - const stream = new Duplex(); - stream.push(buffer); - stream.push(null); - return stream; -} - export function buildProjection(fields: string[]): Record { const o = {}; for (const k of fields) o[k] = 1; return o; } -export function sleep(timeout: number) { - return new Promise((resolve) => { - setTimeout(resolve, timeout); - }); -} - -function deepen(modifyString: (source: string) => string) { - function modifyObject(source: T): T { - if (typeof source !== 'object' || !source) return source; - if (Array.isArray(source)) return source.map(modifyObject) as any; - const result = {} as T; - for (const key in source) { - result[modifyString(key)] = modifyObject(source[key]); - } - return result; - } - - return function t(source: T): T { - if (typeof source === 'string') return modifyString(source) as any; - return modifyObject(source); - }; -} - -export function logAndReturn(logger: Logger) { - return function cb(err: Error) { - logger.error(err); - return err; - }; -} - -export const camelCase = deepen((source) => source.replace(/[_-][a-z]/g, (str) => str.slice(1).toUpperCase())); -export const paramCase = deepen((source) => source.replace(/_/g, '-').replace(/(? `-${str.toLowerCase()}`)); -export const snakeCase = deepen((source) => source.replace(/-/g, '_').replace(/(? `_${str.toLowerCase()}`)); - -export 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`; - } - - export function getObjectID(timestamp: string | Date | Moment) { - let _timestamp: number; - if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime(); - else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime(); - else _timestamp = timestamp.getTime(); - const hexSeconds = Math.floor(_timestamp / 1000).toString(16); - const constructedObjectId = new ObjectID(`${hexSeconds}0000000000000000`); - return constructedObjectId; - } -} - export const log2 = (val: bigint | number) => { if (typeof val === 'bigint') { // @ts-ignore @@ -250,19 +21,15 @@ export const log2 = (val: bigint | number) => { } }; -export function errorMessage(err: Error | string) { - const t = typeof err === 'string' ? err : err.stack; - const q = t.split('\n'); - for (let i = 0; i < q.length; i++) { - if (!q[i].startsWith(' at')) continue; - if (q[i].includes(`${sep}@hydrooj${sep}`)) q[i] = q[i].split(`@hydrooj${sep}`)[1]; - else if (q[i].includes(`${sep}hydrooj${sep}`)) q[i] = `hydrooj${sep}${q[i].split(`hydrooj/${sep}`)[1]}`; - } - if (typeof err === 'string') return q.join('\n'); - err.stack = q.join('\n'); - return err; -} - export function ArgMethod(target: any, funcName: string, obj: any) { return obj; } + +export function logAndReturn(logger: Logger) { + return function cb(err: Error) { + logger.error(err); + return err; + }; +} + +export * from '@hydrooj/utils/lib/utils'; diff --git a/packages/hydrooj/src/lib/sysinfo.ts b/packages/utils/lib/sysinfo.ts similarity index 97% rename from packages/hydrooj/src/lib/sysinfo.ts rename to packages/utils/lib/sysinfo.ts index 505f4c25a..8874ec1ec 100644 --- a/packages/hydrooj/src/lib/sysinfo.ts +++ b/packages/utils/lib/sysinfo.ts @@ -52,5 +52,3 @@ export async function update(): Promise<[string, StatusUpdate, StatusFull]> { }, ]; } - -global.Hydro.lib.sysinfo = { get, update }; diff --git a/packages/utils/lib/utils.ts b/packages/utils/lib/utils.ts new file mode 100644 index 000000000..76d5317a0 --- /dev/null +++ b/packages/utils/lib/utils.ts @@ -0,0 +1,298 @@ +import path, { sep } from 'path'; +import fs from 'fs'; +import { Duplex } from 'stream'; +import { ObjectID } from 'bson'; +import { isMoment } from 'moment'; +import type { Moment } from 'moment-timezone'; + +declare global { + interface StringConstructor { + random: (digit?: number) => string; + } + interface String { + format: (...args: Array) => string; + formatFromArray: (args: any[]) => string; + rawformat: (object: any) => string; + } + interface ArrayConstructor { + isDiff: (a: any[], b: any[]) => boolean; + } + interface Date { + format: (fmt?: string) => string; + } + interface Math { + sum: (...args: Array) => number; + } + interface SetConstructor { + isSuperset: (set: Set, subset: Set) => boolean; + intersection: (setA: Set, setB: Set) => Set; + union: (setA: Set, setB: Set) => Set; + } +} + +const dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + +String.random = function random(digit = 32) { + let str = ''; + for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * 62)]; + return str; +}; + +String.prototype.format = function formatStr(...args) { + let result = this; + if (args.length > 0) { + if (args.length === 1 && typeof (args[0]) === 'object') { + for (const key in args) { + if (args[key] !== undefined) { + const reg = new RegExp(`(\\{${key}\\})`, 'g'); + result = result.replace(reg, args[key]); + } + } + } else return this.formatFromArray(args); + } + return result; +}; + +String.prototype.formatFromArray = function formatStr(args) { + let result = this; + for (let i = 0; i < args.length; i++) { + if (args[i] !== undefined) { + const reg = new RegExp(`(\\{)${i}(\\})`, 'g'); + result = result.replace(reg, args[i]); + } + } + return result; +}; + +String.prototype.rawformat = function rawFormat(object) { + const res = this.split('{@}'); + return [res[0], object, res[1]].join(); +}; + +Array.isDiff = function isDiff(a, b) { + if (a.length !== b.length) return true; + a.sort(); + b.sort(); + for (const i in a) { + if (a[i] !== b[i]) return true; + } + return false; +}; + +Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { + let m = this.getMonth() + 1; + if (m < 10) m = `0${m}`; + let d = this.getDate(); + if (d < 10) d = `0${d}`; + let H = this.getHours(); + if (H < 10) H = `0${H}`; + let M = this.getMinutes(); + if (M < 10) M = `0${M}`; + let S = this.getSeconds(); + if (S < 10) S = `0${S}`; + return fmt + .replace('%Y', this.getFullYear()) + .replace('%m', m) + .replace('%d', d) + .replace('%H', H) + .replace('%M', M) + .replace('%S', S); +}; + +Math.sum = function sum(...args) { + let s = 0; + for (const i of args) { + if (i instanceof Array) { + for (const j of i) { + s += j; + } + } else s += i; + } + return s; +}; + +Set.isSuperset = function isSuperset(set, subset) { + for (const elem of subset) { + if (!set.has(elem)) return false; + } + return true; +}; + +Set.union = function Union(setA: Set, setB: Set) { + const union = new Set(setA); + for (const elem of setB) union.add(elem); + return union; +}; + +Set.intersection = function Intersection(setA: Set, setB: Set): Set { + const intersection = new Set(); + for (const elem of setB) { + if (setA.has(elem)) intersection.add(elem); + } + // @ts-ignore + return intersection; +}; + +export function folderSize(folderPath: string) { + // eslint-disable-next-line @typescript-eslint/no-shadow + let size = 0; + const _next = function a(p: string) { + if (p) { + const stats = fs.statSync(p); + if (!stats.isDirectory() || stats.isSymbolicLink()) { + if (!stats.isSymbolicLink()) size += stats.size; + } else { + size += stats.size; + const files = fs.readdirSync(p); + if (Array.isArray(files)) { + files.forEach((file) => { + _next(path.join(p, file)); + }); + } + } + } + }; + _next(folderPath); + return size; +} + +const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i; +const TIME_UNITS = { '': 1000, m: 1, u: 0.001 }; +const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i; +const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 }; + +export function parseTimeMS(str: string) { + const match = TIME_RE.exec(str); + if (!match) throw new Error(`${str} error parsing time`); + return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); +} + +export function parseMemoryMB(str: string) { + const match = MEMORY_RE.exec(str); + if (!match) throw new Error(`${str} error parsing memory`); + return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); +} + +export function isClass(obj: any, strict = false) { + if (typeof obj !== 'function') return false; + const str = obj.toString(); + if (obj.prototype === undefined) return false; + if (obj.prototype.constructor !== obj) return false; + if (str.slice(0, 5) === 'class') return true; + if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true; + if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false; + if (strict && /^function\s+[A-Z]/.test(str)) return true; + if (/\b\(this\b|\bthis[.[]\b/.test(str)) { + if (!strict || /classCallCheck\(this/.test(str)) return true; + return /^function\sdefault_\d+\s*\(/.test(str); + } + return false; +} + +export function streamToBuffer(stream): Promise { + return new Promise((resolve, reject) => { + const buffers = []; + stream.on('error', reject); + stream.on('data', (data) => buffers.push(data)); + stream.on('end', () => resolve(Buffer.concat(buffers))); + }); +} + +export function bufferToStream(buffer: Buffer): NodeJS.ReadableStream { + const stream = new Duplex(); + stream.push(buffer); + stream.push(null); + return stream; +} + +export function sleep(timeout: number) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +function deepen(modifyString: (source: string) => string) { + function modifyObject(source: T): T { + if (typeof source !== 'object' || !source) return source; + if (Array.isArray(source)) return source.map(modifyObject) as any; + const result = {} as T; + for (const key in source) { + result[modifyString(key)] = modifyObject(source[key]); + } + return result; + } + + return function t(source: T): T { + if (typeof source === 'string') return modifyString(source) as any; + return modifyObject(source); + }; +} + +export function noop() { } + +export const camelCase = deepen((source) => source.replace(/[_-][a-z]/g, (str) => str.slice(1).toUpperCase())); +export const paramCase = deepen((source) => source.replace(/_/g, '-').replace(/(? `-${str.toLowerCase()}`)); +export const snakeCase = deepen((source) => source.replace(/-/g, '_').replace(/(? `_${str.toLowerCase()}`)); + +export 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`; + } + + export function getObjectID(timestamp: string | Date | Moment) { + let _timestamp: number; + if (typeof timestamp === 'string') _timestamp = new Date(timestamp).getTime(); + else if (isMoment(timestamp)) _timestamp = timestamp.toDate().getTime(); + else _timestamp = timestamp.getTime(); + const hexSeconds = Math.floor(_timestamp / 1000).toString(16); + const constructedObjectId = new ObjectID(`${hexSeconds}0000000000000000`); + return constructedObjectId; + } +} + +export function errorMessage(err: Error | string) { + const t = typeof err === 'string' ? err : err.stack; + const q = t.split('\n'); + for (let i = 0; i < q.length; i++) { + if (!q[i].startsWith(' at')) continue; + if (q[i].includes(`${sep}@hydrooj${sep}`)) q[i] = q[i].split(`@hydrooj${sep}`)[1]; + else if (q[i].includes(`${sep}hydrooj${sep}`)) q[i] = `hydrooj${sep}${q[i].split(`hydrooj/${sep}`)[1]}`; + } + if (typeof err === 'string') return q.join('\n'); + err.stack = q.join('\n'); + return err; +} + +export function size(s: number, base = 1) { + s *= base; + const unit = 1024; + const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + for (const unitName of unitNames) { + if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName); + s /= unit; + } + return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`; +} + +function _digit2(number: number) { + return number < 10 ? `0${number}` : number.toString(); +} + +export function formatSeconds(_seconds = '0') { + const seconds = parseInt(_seconds, 10); + return '{0}:{1}:{2}'.format( + _digit2(Math.floor(seconds / 3600)), + _digit2(Math.floor((seconds % 3600) / 60)), + _digit2(seconds % 60), + ); +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..1e06c4b68 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,17 @@ +{ + "name": "@hydrooj/utils", + "version": "1.0.0", + "description": "hydrooj utils", + "main": "package.json", + "repository": "https://github.com/hydro-dev/Hydro.git", + "author": "undefined ", + "license": "AGPL-3.0-only", + "dependencies": { + "bson": "^4.2.3", + "moment": "^2.29.1", + "systeminformation": "^5.6.10" + }, + "devDependencies": { + "moment-timezone": "^0.5.33" + } +} \ No newline at end of file