vjudge: add CheckService (#599)

This commit is contained in:
panda authored and GitHub committed 2023-06-30 00:52:25 +08:00
1 parent 807bfb53e3
commit 9da4340a97
8 files changed
+129 -62

No files matched your search

-59
View File
@@ -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<void>;
const checks: Dictionary<CheckItem> = {
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;
}
+1 -1
View File
@@ -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);
+2
View File
@@ -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<C extends Context = Context> extends cordis.Events<C>, 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;
+1
View File
@@ -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);
+2 -2
View File
@@ -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() {
+78
View File
@@ -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<void>;
export class CheckService extends Service {
checkers: Dictionary<CheckItem> = {};
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;
}
+18
View File
@@ -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
+27
View File
@@ -15,6 +15,8 @@ class AccountService {
api: IBasicProvider;
problemLists: Set<string>;
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<string, { working: boolean, error?: string, status?: any }> = {};
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(', ')}`);
});
}