core: add hydrooj diag

This commit is contained in:
undefined committed 2025-10-29 02:57:27 +08:00
1 parent 3a32b2457b
commit c416c1893e
4 files changed
+157 -20

No files matched your search

+1
View File
@@ -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) {
+2 -2
View File
@@ -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",
+135
View File
@@ -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);
}
+19 -18
View File
@@ -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');