import { Injectable, Logger } from '@nestjs/common'; import * as os from 'os'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); interface ServerMetrics { hostname: string; cpu: { model: string; cores: number; usagePercent: number; loadAvg: number[] }; memory: { total: string; used: string; free: string; percent: number }; disk: { total: string; used: string; free: string; percent: number }; uptime: string; processes: { pid: number; cpu: string; mem: string; command: string }[]; network: { ip: string }; } const REMOTE_SSH = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i /home/ubuntu/.ssh/zhixi.pem ubuntu@10.2.0.7'; const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/zhixi.pem'; @Injectable() export class AdminServersService { private readonly logger = new Logger(AdminServersService.name); async getLocalMetrics(): Promise { const cpus = os.cpus(); const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; // CPU usage (approximate via load avg vs cores) const loadAvg = os.loadavg(); const cpuUsage = Math.round((loadAvg[0] / cpus.length) * 100); // Disk let disk = { total: '-', used: '-', free: '-', percent: 0 }; try { const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'"); const [total, used, free, pct] = stdout.trim().split(/\s+/); disk = { total, used, free, percent: parseInt(pct) || 0 }; } catch {} // Top processes let processes: ServerMetrics['processes'] = []; try { const { stdout } = await execAsync("ps aux --sort=-%mem --no-headers | head -8 | awk '{print $2,$3,$4,$11}'"); processes = stdout.trim().split('\n').map(line => { const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: cmd.join(' ').slice(0, 60) }; }); } catch {} // Network IPs const nets = os.networkInterfaces(); const ip = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || 'unknown'; // Uptime const uptimeSeconds = os.uptime(); const d = Math.floor(uptimeSeconds / 86400); const h = Math.floor((uptimeSeconds % 86400) / 3600); const m = Math.floor((uptimeSeconds % 3600) / 60); const uptime = `${d}d ${h}h ${m}m`; return { hostname: os.hostname(), cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuUsage, loadAvg }, memory: { total: (totalMem / 1e9).toFixed(1) + 'G', used: (usedMem / 1e9).toFixed(1) + 'G', free: (freeMem / 1e9).toFixed(1) + 'G', percent: Math.round((usedMem / totalMem) * 100) }, disk, uptime, processes, network: { ip }, }; } async getRemoteMetrics(): Promise { try { const sshKey = SSH_KEY_PATH; const cmd = `${REMOTE_SSH} 'echo "HOST=$(hostname)"; echo "IP=$(hostname -I | awk '"'"'{print \$1}'"'"')"; echo "UPTIME=$(uptime -p)"; top -bn1 | head -1; free -h | grep Mem; df -h / | tail -1; ps aux --sort=-%mem --no-headers | head -6 | awk '"'"'{print \$2,\$3,\$4,\$11}'"'"'`; const { stdout } = await execAsync(cmd.replace('ssh -o', `ssh -i ${sshKey} -o`), { timeout: 8000 }); const lines = stdout.trim().split('\n'); const hostname = lines.find(l => l.startsWith('HOST='))?.split('=')[1] || 'remote'; const ip = lines.find(l => l.startsWith('IP='))?.split('=')[1] || '10.2.0.7'; const uptimeStr = lines.find(l => l.startsWith('UPTIME='))?.split('=')[1]?.replace('up ', '') || ''; // top output: "load average: 0.08, 0.03, 0.01" const topLine = lines.find(l => l.includes('load average')) || ''; const loadMatch = topLine.match(/load average: ([\d.]+), ([\d.]+), ([\d.]+)/); const loadAvg = loadMatch ? [parseFloat(loadMatch[1]), parseFloat(loadMatch[2]), parseFloat(loadMatch[3])] : [0, 0, 0]; // memory const memLine = lines.find(l => /Mem:/.test(l)) || ''; const memParts = memLine.replace('Mem:', '').trim().split(/\s+/); // disk const diskLine = lines.find(l => /\/$/.test(l) || l.includes('/ ')) || ''; const diskParts = diskLine.trim().split(/\s+/); // processes const procLines = lines.filter(l => /^\d+\s/.test(l)); const cpuUsage = Math.round((loadAvg[0] / 4) * 100); // assume 4 cores return { hostname, cpu: { model: 'Intel Xeon (Lighthouse)', cores: 4, usagePercent: cpuUsage, loadAvg }, memory: { total: memParts[1] || '-', used: memParts[2] || '-', free: memParts[3] || '-', percent: loadAvg[0] > 0 ? Math.round(cpuUsage) : 0, }, disk: { total: diskParts[1] || '-', used: diskParts[2] || '-', free: diskParts[3] || '-', percent: parseInt(diskParts[4]) || 0, }, uptime: uptimeStr, processes: procLines.map(line => { const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: (cmd || []).join(' ').slice(0, 60) }; }), network: { ip }, }; } catch (err: any) { this.logger.warn('Remote metrics failed: ' + err.message); return null; } } async getAllMetrics() { const [local, remote] = await Promise.all([ this.getLocalMetrics(), this.getRemoteMetrics(), ]); return { servers: [ { name: '蜂驰云 8核32G', role: '生产核心', ...local }, ...(remote ? [{ name: '轻量云 4核4G', role: '工具/辅助', ...remote }] : []), ], }; } }