From 9da4340a9709a57489176aa84a117886f3bb819a Mon Sep 17 00:00:00 2001 From: panda Date: Fri, 30 Jun 2023 00:52:25 +0800 Subject: [PATCH] vjudge: add CheckService (#599) --- packages/hydrooj/src/check.ts | 59 ------------------ packages/hydrooj/src/commands/install.ts | 2 +- packages/hydrooj/src/context.ts | 2 + packages/hydrooj/src/entry/worker.ts | 1 + packages/hydrooj/src/handler/manage.ts | 4 +- packages/hydrooj/src/service/check.ts | 78 ++++++++++++++++++++++++ packages/hydrooj/src/service/storage.ts | 18 ++++++ packages/vjudge/src/model.ts | 27 ++++++++ 8 files changed, 129 insertions(+), 62 deletions(-) delete mode 100644 packages/hydrooj/src/check.ts create mode 100644 packages/hydrooj/src/service/check.ts diff --git a/packages/hydrooj/src/check.ts b/packages/hydrooj/src/check.ts deleted file mode 100644 index ca9784b5d..000000000 --- a/packages/hydrooj/src/check.ts +++ /dev/null @@ -1,59 +0,0 @@ -// TODO check more - -import os from 'os'; -import { Dictionary } from 'lodash'; -import * as system from './model/system'; -import db from './service/db'; - -const c = {}; - -type CheckItem = (context: any, log: Function, warn: Function, error: Function) => Promise; - -const checks: Dictionary = { - async checkDb(ctx, log, warn, error) { - try { - const coll = db.collection('check'); - const d = await coll.findOne({ _id: 'check' }); - if (!d) await coll.insertOne({ _id: 'check', value: 'check' }); - await coll.createIndex('check'); - await coll.deleteOne({ _id: 'check' }); - await coll.drop(); - } catch (e) { - error(`Mongo Error: Database read-write failed.\n${e.message}`); - } - }, - // TODO check storage - async checkSystem(ctx, log, warn) { - const platform = os.platform(); - if (platform !== 'linux') { - warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`); - } - }, - async checkMail(ctx, log, warn) { - const from = system.get('smtp.from'); - if (!from) warn('SMTP account is not provided, email verification disabled.'); - }, - async checkSetting(ctx, log, warn) { - const url = system.get('server.url'); - if (url === '/') warn("server.url isn't set."); - const header = system.get('server.xff'); - if (header && !ctx.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.'); - }, -}; - -export async function start(ctx, log, warn, error, cb: (id: string) => void) { - const id = String.random(6); - cb(id); - for (const name in checks) { - if (c[id]) { - delete c[id]; - return; - } - // eslint-disable-next-line no-await-in-loop - await checks[name](ctx, log, warn, error); - } -} - -export async function cancel(id: string) { - c[id] = true; -} diff --git a/packages/hydrooj/src/commands/install.ts b/packages/hydrooj/src/commands/install.ts index f1c9e3843..39afa9840 100644 --- a/packages/hydrooj/src/commands/install.ts +++ b/packages/hydrooj/src/commands/install.ts @@ -45,7 +45,7 @@ export function register(cli: CAC) { const url = new URL(src); const filename = url.pathname.split('/').pop()!; if (['.tar.gz', '.tgz', '.zip'].find((i) => filename.endsWith(i))) { - const name = filename.replace(/(-?\d+\.\d+\.\d+)?(\.tar\.gz|\.zip|\.tgz)$/g, ''); + const name = filename.replace(/(-?(\d+\.\d+\.\d+|latest))?(\.tar\.gz|\.zip|\.tgz)$/g, ''); newAddonPath = path.join(addonDir, name); logger.info(`Downloading ${src} to ${newAddonPath}`); fs.ensureDirSync(newAddonPath); diff --git a/packages/hydrooj/src/context.ts b/packages/hydrooj/src/context.ts index ae38c2c69..7f9329b65 100644 --- a/packages/hydrooj/src/context.ts +++ b/packages/hydrooj/src/context.ts @@ -4,6 +4,7 @@ import { GeoIP, ModuleInterfaces } from './interface'; import { inject } from './lib/ui'; import { Loader } from './loader'; import { EventMap } from './service/bus'; +import type { CheckService } from './service/check'; export interface Events extends cordis.Events, EventMap { } @@ -26,6 +27,7 @@ export interface Context { Route: typeof import('./service/server').Route; Connection: typeof import('./service/server').Connection; withHandlerClass: import('./service/server').RouteService['withHandlerClass']; + check: CheckService; setTimeout: typeof setTimeout; setInterval: typeof setInterval; setImmediate: typeof setImmediate; diff --git a/packages/hydrooj/src/entry/worker.ts b/packages/hydrooj/src/entry/worker.ts index bde3a548d..610aaf3f0 100644 --- a/packages/hydrooj/src/entry/worker.ts +++ b/packages/hydrooj/src/entry/worker.ts @@ -48,6 +48,7 @@ export async function apply(ctx: Context) { await setting(pending, fail, require('../model/setting')); ctx.plugin(require('../service/monitor')); + ctx.plugin(require('../service/check')); await service(pending, fail, ctx); await builtinModel(ctx); await model(pending, fail, ctx); diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index a3d2d03f2..4215d2830 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -3,7 +3,6 @@ import { inspect } from 'util'; import * as yaml from 'js-yaml'; import { omit } from 'lodash'; import Schema from 'schemastery'; -import * as check from '../check'; import { CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError, } from '../error'; @@ -14,6 +13,7 @@ import record from '../model/record'; import * as setting from '../model/setting'; import * as system from '../model/system'; import user from '../model/user'; +import { check } from '../service/check'; import { ConnectionHandler, Handler, param, requireSudo, Types, } from '../service/server'; @@ -71,7 +71,7 @@ class SystemCheckConnHandler extends ConnectionHandler { const log = (payload: any) => this.send({ type: 'log', payload }); const warn = (payload: any) => this.send({ type: 'warn', payload }); const error = (payload: any) => this.send({ type: 'error', payload }); - await check.start(this, log, warn, error, (id) => { this.id = id; }); + await check.run(this, log, warn, error, (id) => { this.id = id; }); } async cleanup() { diff --git a/packages/hydrooj/src/service/check.ts b/packages/hydrooj/src/service/check.ts new file mode 100644 index 000000000..997e97d8c --- /dev/null +++ b/packages/hydrooj/src/service/check.ts @@ -0,0 +1,78 @@ +// TODO check more + +import os from 'os'; +import { Dictionary } from 'lodash'; +import { Context, Service } from '../context'; +import * as system from '../model/system'; +import db from './db'; +import storage from './storage'; + +type CheckItem = (context: any, log: Function, warn: Function, error: Function) => Promise; + +export class CheckService extends Service { + checkers: Dictionary = {}; + c = {}; + addChecker(type: string, checkFunc: CheckItem) { + this.checkers[`check${type}`] = checkFunc; + this.caller?.on('dispose', () => { + delete this.checkers[`check${type}`]; + }); + } + + async run(ctx, log, warn, error, cb: (id: string) => void) { + const id = String.random(6); + cb(id); + for (const name in this.checkers) { + if (this.c[id]) { + delete this.c[id]; + return; + } + // eslint-disable-next-line no-await-in-loop + await this.checkers[name](ctx, log, warn, error); + } + } + + async cancel(id: string) { + this.c[id] = true; + } +} + +export const check = new CheckService(app, 'check', true); + +check.addChecker('Db', async (ctx, log, warn, error) => { + try { + const coll = db.collection('check'); + const d = await coll.findOne({ _id: 'check' }); + if (!d) await coll.insertOne({ _id: 'check', value: 'check' }); + await coll.createIndex('check'); + await coll.deleteOne({ _id: 'check' }); + await coll.drop(); + } catch (e) { + error(`Mongo Error: Database read-write failed.\n${e.message}`); + } +}); +check.addChecker('Storage', async (ctx, log, warn, error) => { + const status = await storage.status(); + if (!status.status) error(`Connect to ${status.type} Storage ${status.bucket} Error: ${status.error}`); +}); +check.addChecker('System', async (ctx, log, warn) => { + const platform = os.platform(); + if (platform !== 'linux') { + warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`); + } +}); +check.addChecker('Mail', async (ctx, log, warn) => { + const from = system.get('smtp.from'); + if (!from) warn('SMTP account is not provided, email verification disabled.'); +}); +check.addChecker('Setting', async (ctx, log, warn) => { + const url = system.get('server.url'); + if (url === '/') warn("server.url isn't set."); + const header = system.get('server.xff'); + if (header && !ctx.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.'); +}); + +export async function apply(ctx: Context) { + Context.service('check', CheckService); + ctx.check = check; +} diff --git a/packages/hydrooj/src/service/storage.ts b/packages/hydrooj/src/service/storage.ts index 77c523276..e12d128f5 100644 --- a/packages/hydrooj/src/service/storage.ts +++ b/packages/hydrooj/src/service/storage.ts @@ -252,6 +252,15 @@ class RemoteStorageService { CopySource: src, })); } + + async status() { + return { + type: 'S3', + status: !!this.error, + error: this.error, + bucket: this.bucket, + }; + } } class LocalStorageService { @@ -329,6 +338,15 @@ class LocalStorageService { await copyFile(src, target); return { etag: target, lastModified: new Date() }; } + + async status() { + return { + type: 'Local', + status: !!this.error, + error: this.error, + bucket: 'Hydro', + }; + } } let service; // eslint-disable-line import/no-mutable-exports diff --git a/packages/vjudge/src/model.ts b/packages/vjudge/src/model.ts index 8b9c97138..35dd67987 100644 --- a/packages/vjudge/src/model.ts +++ b/packages/vjudge/src/model.ts @@ -15,6 +15,8 @@ class AccountService { api: IBasicProvider; problemLists: Set; listUpdated = false; + working = false; + error = null; constructor(public Provider: BasicProvider, public account: RemoteAccount) { this.api = new Provider(account, async (data) => { @@ -23,6 +25,7 @@ class AccountService { this.problemLists = Set.union(this.api.entryProblemLists || ['main'], this.account.problemLists || []); this.main().catch((e) => { logger.error(`Error occured in ${account.type}/${account.handle}`); + this.error = e.message; console.error(e); }); } @@ -121,6 +124,7 @@ class AccountService { setInterval(() => this.login(), Time.hour); TaskModel.consume({ type: 'remotejudge', subType: this.account.type.split('.')[0] }, this.judge.bind(this), false); const ddocs = await DomainModel.getMulti({ mount: this.account.type.split('.')[0] }).toArray(); + this.working = true; do { this.listUpdated = false; for (const listName of this.problemLists) { @@ -170,6 +174,18 @@ class VJudgeService extends Service { // TODO dispose session }); } + + async checkStatus(onCheckFunc = false) { + const res: Record = {}; + for (const [k, v] of Object.entries(this.pool)) { + res[k] = { + working: v.working, + error: v.error, + status: v.api.checkStatus ? await v.api.checkStatus(onCheckFunc) : null, + }; + } + return res; + } } export { BasicFetcher } from './fetch'; @@ -189,4 +205,15 @@ export async function apply(ctx: Context) { } // }); ctx.vjudge = vjudge; + ctx.check.addChecker('Vjudge', async (_ctx, log, warn, error) => { + const working = []; + const pool = await vjudge.checkStatus(true); + for (const [k, v] of Object.entries(pool)) { + if (!v.working) error(`vjudge worker ${k}: ${v.error}`); + else working.push(k); + if (v.status) log(`vjudge worker ${k}: ${v.status}`); + } + if (!working.length) warn('no vjudge worker is working'); + log(`vjudge working workers: ${working.join(', ')}`); + }); }