mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
vjudge: add CheckService (#599)
This commit is contained in:
1 parent
807bfb53e3
commit
9da4340a97
8 files changed
+129
-62
No files matched your search
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,7 @@ export function register(cli: CAC) {
|
|||||||
const url = new URL(src);
|
const url = new URL(src);
|
||||||
const filename = url.pathname.split('/').pop()!;
|
const filename = url.pathname.split('/').pop()!;
|
||||||
if (['.tar.gz', '.tgz', '.zip'].find((i) => filename.endsWith(i))) {
|
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);
|
newAddonPath = path.join(addonDir, name);
|
||||||
logger.info(`Downloading ${src} to ${newAddonPath}`);
|
logger.info(`Downloading ${src} to ${newAddonPath}`);
|
||||||
fs.ensureDirSync(newAddonPath);
|
fs.ensureDirSync(newAddonPath);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GeoIP, ModuleInterfaces } from './interface';
|
|||||||
import { inject } from './lib/ui';
|
import { inject } from './lib/ui';
|
||||||
import { Loader } from './loader';
|
import { Loader } from './loader';
|
||||||
import { EventMap } from './service/bus';
|
import { EventMap } from './service/bus';
|
||||||
|
import type { CheckService } from './service/check';
|
||||||
|
|
||||||
export interface Events<C extends Context = Context> extends cordis.Events<C>, EventMap {
|
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;
|
Route: typeof import('./service/server').Route;
|
||||||
Connection: typeof import('./service/server').Connection;
|
Connection: typeof import('./service/server').Connection;
|
||||||
withHandlerClass: import('./service/server').RouteService['withHandlerClass'];
|
withHandlerClass: import('./service/server').RouteService['withHandlerClass'];
|
||||||
|
check: CheckService;
|
||||||
setTimeout: typeof setTimeout;
|
setTimeout: typeof setTimeout;
|
||||||
setInterval: typeof setInterval;
|
setInterval: typeof setInterval;
|
||||||
setImmediate: typeof setImmediate;
|
setImmediate: typeof setImmediate;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export async function apply(ctx: Context) {
|
|||||||
|
|
||||||
await setting(pending, fail, require('../model/setting'));
|
await setting(pending, fail, require('../model/setting'));
|
||||||
ctx.plugin(require('../service/monitor'));
|
ctx.plugin(require('../service/monitor'));
|
||||||
|
ctx.plugin(require('../service/check'));
|
||||||
await service(pending, fail, ctx);
|
await service(pending, fail, ctx);
|
||||||
await builtinModel(ctx);
|
await builtinModel(ctx);
|
||||||
await model(pending, fail, ctx);
|
await model(pending, fail, ctx);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { inspect } from 'util';
|
|||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import Schema from 'schemastery';
|
import Schema from 'schemastery';
|
||||||
import * as check from '../check';
|
|
||||||
import {
|
import {
|
||||||
CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError,
|
CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
@@ -14,6 +13,7 @@ import record from '../model/record';
|
|||||||
import * as setting from '../model/setting';
|
import * as setting from '../model/setting';
|
||||||
import * as system from '../model/system';
|
import * as system from '../model/system';
|
||||||
import user from '../model/user';
|
import user from '../model/user';
|
||||||
|
import { check } from '../service/check';
|
||||||
import {
|
import {
|
||||||
ConnectionHandler, Handler, param, requireSudo, Types,
|
ConnectionHandler, Handler, param, requireSudo, Types,
|
||||||
} from '../service/server';
|
} from '../service/server';
|
||||||
@@ -71,7 +71,7 @@ class SystemCheckConnHandler extends ConnectionHandler {
|
|||||||
const log = (payload: any) => this.send({ type: 'log', payload });
|
const log = (payload: any) => this.send({ type: 'log', payload });
|
||||||
const warn = (payload: any) => this.send({ type: 'warn', payload });
|
const warn = (payload: any) => this.send({ type: 'warn', payload });
|
||||||
const error = (payload: any) => this.send({ type: 'error', 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() {
|
async cleanup() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -252,6 +252,15 @@ class RemoteStorageService {
|
|||||||
CopySource: src,
|
CopySource: src,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async status() {
|
||||||
|
return {
|
||||||
|
type: 'S3',
|
||||||
|
status: !!this.error,
|
||||||
|
error: this.error,
|
||||||
|
bucket: this.bucket,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalStorageService {
|
class LocalStorageService {
|
||||||
@@ -329,6 +338,15 @@ class LocalStorageService {
|
|||||||
await copyFile(src, target);
|
await copyFile(src, target);
|
||||||
return { etag: target, lastModified: new Date() };
|
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
|
let service; // eslint-disable-line import/no-mutable-exports
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class AccountService {
|
|||||||
api: IBasicProvider;
|
api: IBasicProvider;
|
||||||
problemLists: Set<string>;
|
problemLists: Set<string>;
|
||||||
listUpdated = false;
|
listUpdated = false;
|
||||||
|
working = false;
|
||||||
|
error = null;
|
||||||
|
|
||||||
constructor(public Provider: BasicProvider, public account: RemoteAccount) {
|
constructor(public Provider: BasicProvider, public account: RemoteAccount) {
|
||||||
this.api = new Provider(account, async (data) => {
|
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.problemLists = Set.union(this.api.entryProblemLists || ['main'], this.account.problemLists || []);
|
||||||
this.main().catch((e) => {
|
this.main().catch((e) => {
|
||||||
logger.error(`Error occured in ${account.type}/${account.handle}`);
|
logger.error(`Error occured in ${account.type}/${account.handle}`);
|
||||||
|
this.error = e.message;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,6 +124,7 @@ class AccountService {
|
|||||||
setInterval(() => this.login(), Time.hour);
|
setInterval(() => this.login(), Time.hour);
|
||||||
TaskModel.consume({ type: 'remotejudge', subType: this.account.type.split('.')[0] }, this.judge.bind(this), false);
|
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();
|
const ddocs = await DomainModel.getMulti({ mount: this.account.type.split('.')[0] }).toArray();
|
||||||
|
this.working = true;
|
||||||
do {
|
do {
|
||||||
this.listUpdated = false;
|
this.listUpdated = false;
|
||||||
for (const listName of this.problemLists) {
|
for (const listName of this.problemLists) {
|
||||||
@@ -170,6 +174,18 @@ class VJudgeService extends Service {
|
|||||||
// TODO dispose session
|
// 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';
|
export { BasicFetcher } from './fetch';
|
||||||
@@ -189,4 +205,15 @@ export async function apply(ctx: Context) {
|
|||||||
}
|
}
|
||||||
// });
|
// });
|
||||||
ctx.vjudge = vjudge;
|
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(', ')}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user