mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: add hydrooj diag
This commit is contained in:
4 files changed
+157
-20
No files matched your search
@@ -15,6 +15,7 @@ if (!argv.args[0] || argv.args[0] === 'cli') {
|
||||
require('../src/commands/addon').register(cli);
|
||||
require('../src/commands/db').register(cli);
|
||||
require('../src/commands/patch').register(cli);
|
||||
require('../src/commands/diagnosis').register(cli);
|
||||
cli.help();
|
||||
cli.parse();
|
||||
if (!cli.matchedCommand) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"mongodb": "^6.20.0",
|
||||
"mongodb-uri": "^0.9.7",
|
||||
"nanoid": "^5.1.6",
|
||||
"nodemailer": "^7.0.9",
|
||||
"nodemailer": "^7.0.10",
|
||||
"notp": "^2.0.3",
|
||||
"p-queue": "^9.0.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/mongodb-uri": "^0.9.4",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/notp": "^2.0.5",
|
||||
"@types/saslprep": "^1.0.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import child, { execSync } from 'child_process';
|
||||
import dns from 'dns/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
import { CAC, cac } from 'cac';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import superagent from 'superagent';
|
||||
import { formatSeconds, Logger } from '@hydrooj/utils';
|
||||
import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
||||
import { getAddons } from '../options';
|
||||
|
||||
const logger = new Logger('diagnosis');
|
||||
const argv = cac().parse();
|
||||
|
||||
export function register(cli: CAC) {
|
||||
let baseInfo: sysinfo.StatusFull;
|
||||
let endpoints: string[] = ['hydro.ac'];
|
||||
|
||||
const padEnd = (str: string, length: number) => {
|
||||
return str.length >= length ? `${str} ` : str.toString().padEnd(length, ' ');
|
||||
};
|
||||
|
||||
const collectors: Record<string, () => [any, any] | Promise<[any, any]>> = {
|
||||
OS: () => {
|
||||
const info = fs.readFileSync('/etc/os-release', 'utf-8');
|
||||
const prettyName = info.split('\n').find((i) => i.startsWith('PRETTY_NAME=')).split('=')[1].trim()
|
||||
|| `${baseInfo.osinfo.distro} ${baseInfo.osinfo.release} ${baseInfo.osinfo.arch}`;
|
||||
return [`${prettyName} ${baseInfo.osinfo.kernel}`, info];
|
||||
},
|
||||
CPU: () => {
|
||||
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf-8');
|
||||
const modelName = cpuinfo.split('\n').find((i) => i.startsWith('model name')).split(':')[1].trim();
|
||||
return [
|
||||
modelName,
|
||||
cpuinfo.split('\n'),
|
||||
];
|
||||
},
|
||||
Memory: () => {
|
||||
const total = Math.floor(baseInfo.memory.total / 1024 / 1024);
|
||||
const used = Math.floor((baseInfo.memory.total - baseInfo.memory.available) / 1024 / 1024);
|
||||
const info = `${used}/${total}MB`;
|
||||
let dmidecode = '';
|
||||
try {
|
||||
const result = child.spawnSync('dmidecode', ['-t', 'memory']);
|
||||
dmidecode = `${result.stdout.toString()}\n${result.stderr.toString()}`.trim();
|
||||
} catch (e) {
|
||||
dmidecode = e.message;
|
||||
}
|
||||
return [info, `${fs.readFileSync('/proc/meminfo', 'utf-8')}\n${dmidecode}`.split('\n')];
|
||||
},
|
||||
NodeJS: () => {
|
||||
const info = `${process.versions.node} (${process.execPath})`;
|
||||
return [info, info];
|
||||
},
|
||||
Network: async () => {
|
||||
const ips = await dns.resolve4('hydro.ac');
|
||||
const info = [`hydro.ac: ${ips.join(', ')}`];
|
||||
try {
|
||||
const res = await superagent.get('https://hydro.ac/hydroac-client-version');
|
||||
info.push(`connect: ${res.status} ${!!res.body.version}`);
|
||||
if (res.body.endpoints?.length) endpoints = res.body.endpoints;
|
||||
} catch (e) { }
|
||||
return [info.join('\n'), info.join('\n')];
|
||||
},
|
||||
PM2: async () => {
|
||||
const result = child.spawnSync('pm2', ['jlist']);
|
||||
if (result.stderr?.toString()?.trim()) {
|
||||
return [result.stderr.toString(), result.stderr.toString()];
|
||||
}
|
||||
const json = JSON.parse(result.stdout.toString());
|
||||
const processes = json.map((i) => `PID=${padEnd(i.pid || 0, 7)}${padEnd(i.name, 8)}\
|
||||
${padEnd(formatSeconds(Math.floor((Date.now() - i.pm2_env.pm_uptime) / 1000), false), 8)}\
|
||||
(${i.pm2_env.pm_exec_path} ${(i.pm2_env.args || []).join(' ')})`);
|
||||
return [processes, processes];
|
||||
},
|
||||
HydroOJ: () => {
|
||||
const info = `${require('hydrooj/package.json').version}${process.env.HYDRO_PROFILE ? ` (${process.env.HYDRO_PROFILE})` : ''}`;
|
||||
const addons = getAddons();
|
||||
const addonsInfo = Object.fromEntries(addons.map((i) => {
|
||||
try {
|
||||
const pkgJson = require(`${i}/package.json`);
|
||||
return [i, pkgJson.version];
|
||||
} catch (e) {
|
||||
return [i, e.message];
|
||||
}
|
||||
}));
|
||||
return [
|
||||
{ Hydro: info, ...addonsInfo }, { Hydro: info, ...addonsInfo },
|
||||
];
|
||||
},
|
||||
Logs: () => {
|
||||
const logs = execSync('pm2 logs --lines 1000 --nostream', { maxBuffer: 100 * 1024 * 1024 });
|
||||
return ['Omitted, view in detail mode', logs.toString().split('\n')];
|
||||
},
|
||||
};
|
||||
|
||||
const command = async () => {
|
||||
logger.info('Collecting system info');
|
||||
const infos = {};
|
||||
const infoDetails = {};
|
||||
baseInfo = await sysinfo.get();
|
||||
await Promise.all(Object.entries(collectors).map(async ([key, value]) => {
|
||||
try {
|
||||
const [info, detail] = await value();
|
||||
infos[key] = argv.options.detail ? detail : info;
|
||||
infoDetails[key] = detail;
|
||||
} catch (e) {
|
||||
infos[key] = e.message;
|
||||
infoDetails[key] = e.message;
|
||||
}
|
||||
}));
|
||||
logger.info('System info:');
|
||||
console.log(yaml.dump(infos, { noCompatMode: true, noArrayIndent: true, lineWidth: process.stdout.columns || 80 }));
|
||||
logger.info('Uploading detail to pastebin...');
|
||||
const tmpFile = path.join(tmpdir(), 'report.yaml');
|
||||
fs.writeFileSync(tmpFile, yaml.dump(infoDetails, { noArrayIndent: true, lineWidth: 150 }));
|
||||
if (process.env.VSCODE_INJECTION) {
|
||||
try {
|
||||
child.exec(`code ${tmpFile}`);
|
||||
} catch (e) { }
|
||||
try {
|
||||
child.exec(`cursor ${tmpFile}`);
|
||||
} catch (err) { }
|
||||
}
|
||||
const res = await superagent.post(`https://${endpoints[0]}/paste?code=dm`).attach('file', tmpFile);
|
||||
const [url, , ...rest] = res.text.split('\n');
|
||||
logger.info(`Info created on ${url}`);
|
||||
logger.info('Check logs content before sharing this url.');
|
||||
console.log(rest.join('\n'));
|
||||
};
|
||||
|
||||
cli.command('diag').option('-d', 'detail').action(command);
|
||||
cli.command('diagnosis').option('-d', 'detail').action(command);
|
||||
}
|
||||
@@ -34,7 +34,13 @@ function downloadAndExtractTgz(url: string, dest: string) {
|
||||
});
|
||||
}
|
||||
async function downloadAndExtractZip(url: string, dest: string) {
|
||||
const res = await superagent.get(url).set('User-Agent', userAgent).responseType('arraybuffer');
|
||||
let res;
|
||||
try {
|
||||
res = await superagent.get(url).set('User-Agent', userAgent).responseType('arraybuffer');
|
||||
} catch (e) {
|
||||
logger.error('Failed to download', url, e.message);
|
||||
return;
|
||||
}
|
||||
await extractZip(new ZipReader(new BlobReader(new Blob([res.body]))), dest, { strip: true, overwrite: true });
|
||||
}
|
||||
const types = {
|
||||
@@ -57,27 +63,22 @@ export function register(cli: CAC) {
|
||||
try {
|
||||
src = child.execSync(`yarn info ${src} dist.tarball`, { cwd: os.tmpdir() })
|
||||
.toString().trim().split('\n')[1];
|
||||
if (!src.startsWith('http')) throw new Error();
|
||||
} catch (e) {
|
||||
throw new Error('Cannot fetch package info.');
|
||||
}
|
||||
}
|
||||
if (src.startsWith('@hydrooj/')) {
|
||||
src = child.execSync(`npm info ${src} dist.tarball`, { cwd: os.tmpdir() }).toString().trim();
|
||||
if (!src.startsWith('http')) throw new Error('Cannot fetch package info.');
|
||||
}
|
||||
if (src.startsWith('http')) {
|
||||
const url = new URL(src);
|
||||
const filename = url.pathname.split('/').pop()!;
|
||||
if (Object.keys(types).find((i) => filename.endsWith(i))) {
|
||||
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);
|
||||
fs.emptyDirSync(newAddonPath);
|
||||
const func = types[Object.keys(types).find((i) => filename.endsWith(i))]!;
|
||||
await func(src, newAddonPath);
|
||||
} else throw new Error('Unsupported file type');
|
||||
} else throw new Error(`Unsupported install source: ${src}`);
|
||||
const url = new URL(src);
|
||||
const filename = url.pathname.split('/').pop()!;
|
||||
if (Object.keys(types).find((i) => filename.endsWith(i))) {
|
||||
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);
|
||||
fs.emptyDirSync(newAddonPath);
|
||||
const func = types[Object.keys(types).find((i) => filename.endsWith(i))]!;
|
||||
await func(src, newAddonPath);
|
||||
} else throw new Error('Unsupported file type');
|
||||
if (!newAddonPath) throw new Error('Addon download failed');
|
||||
logger.info('Installing depedencies');
|
||||
if (!fs.existsSync(path.join(newAddonPath, 'package.json'))) throw new Error('Invalid plugin file');
|
||||
|
||||
Reference in new issue
Block a user