Files
Docker-Proxy/hubcmdui/routes/system.js

591 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 系统相关路由 - 使用SQLite数据库
*/
const express = require('express');
const router = express.Router();
const os = require('os'); // 确保导入 os 模块
const util = require('util'); // 导入 util 模块
const { exec } = require('child_process');
const execPromise = util.promisify(exec); // 只在这里定义一次
const logger = require('../logger');
const { requireLogin } = require('../middleware/auth');
const configServiceDB = require('../services/configServiceDB');
const { execCommand, getSystemInfo } = require('../server-utils');
const dockerService = require('../services/dockerService');
const path = require('path');
const fs = require('fs').promises;
// 获取系统状态
async function getSystemStats(req, res) {
try {
let dockerAvailable = false;
let containerCount = '0';
let memoryUsage = '0%';
let cpuLoad = '0%';
let diskSpace = '0%';
let recentActivities = [];
// 尝试获取系统信息
try {
const systemInfo = await getSystemInfo();
memoryUsage = `${systemInfo.memory.percent}%`;
cpuLoad = systemInfo.cpu.load1;
diskSpace = systemInfo.disk.percent;
} catch (sysError) {
logger.error('获取系统信息失败:', sysError);
}
// 尝试从Docker获取状态信息
try {
const docker = await dockerService.getDockerConnection();
if (docker) {
dockerAvailable = true;
// 获取容器统计
const containers = await docker.listContainers({ all: true });
containerCount = containers.length.toString();
// 获取最近的容器活动
const runningContainers = containers.filter(c => c.State === 'running');
for (let i = 0; i < Math.min(3, runningContainers.length); i++) {
recentActivities.push({
time: new Date(runningContainers[i].Created * 1000).toLocaleString(),
action: '运行中',
container: runningContainers[i].Names[0].replace(/^\//, ''),
status: '正常'
});
}
// 获取最近的Docker事件
const events = await dockerService.getRecentEvents();
if (events && events.length > 0) {
recentActivities = [...events.map(event => ({
time: new Date(event.time * 1000).toLocaleString(),
action: event.Action,
container: event.Actor?.Attributes?.name || '未知容器',
status: event.status || '完成'
})), ...recentActivities].slice(0, 10);
}
}
} catch (containerError) {
logger.error('获取容器信息失败:', containerError);
}
// 如果没有活动记录,添加一个默认记录
if (recentActivities.length === 0) {
recentActivities.push({
time: new Date().toLocaleString(),
action: '系统检查',
container: '监控服务',
status: dockerAvailable ? '正常' : 'Docker服务不可用'
});
}
// 返回收集到的所有数据,即使部分数据可能不完整
res.json({
dockerAvailable,
containerCount,
memoryUsage,
cpuLoad,
diskSpace,
recentActivities
});
} catch (error) {
logger.error('获取系统统计数据失败:', error);
// 即使出错,仍然尝试返回一些基本数据
res.status(200).json({
dockerAvailable: false,
containerCount: '0',
memoryUsage: '未知',
cpuLoad: '未知',
diskSpace: '未知',
recentActivities: [{
time: new Date().toLocaleString(),
action: '系统错误',
container: '监控服务',
status: '数据获取失败'
}],
error: '获取系统统计数据失败',
errorDetails: error.message
});
}
}
// 获取系统配置 - 修改版本,避免与其他路由冲突
router.get('/system-config', async (req, res) => {
try {
const config = await configServiceDB.getConfig();
res.json(config);
} catch (error) {
logger.error('读取配置失败:', error);
res.status(500).json({
error: '读取配置失败',
details: error.message
});
}
});
// 保存系统配置 - 修改版本,避免与其他路由冲突
router.post('/system-config', requireLogin, async (req, res) => {
try {
const currentConfig = await configServiceDB.getConfig();
const newConfig = { ...currentConfig, ...req.body };
await configServiceDB.saveConfigs(newConfig);
logger.info('系统配置已更新');
res.json({ success: true });
} catch (error) {
logger.error('保存配置失败:', error);
res.status(500).json({
error: '保存配置失败',
details: error.message
});
}
});
// 获取系统状态
router.get('/stats', requireLogin, async (req, res) => {
return await getSystemStats(req, res);
});
// 获取磁盘空间信息
router.get('/disk-space', requireLogin, async (req, res) => {
try {
const systemInfo = await getSystemInfo();
res.json({
diskSpace: `${systemInfo.disk.used}/${systemInfo.disk.size}`,
usagePercent: parseInt(systemInfo.disk.percent)
});
} catch (error) {
logger.error('获取磁盘空间信息失败:', error);
res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message });
}
});
// 网络测试
router.post('/network-test', requireLogin, async (req, res) => {
const { type, domain } = req.body;
// 验证输入
function validateInput(input, type) {
if (type === 'domain') {
return /^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(input);
}
return false;
}
if (!validateInput(domain, 'domain')) {
return res.status(400).json({ error: '无效的域名格式' });
}
try {
const result = await execCommand(`${type === 'ping' ? 'ping -c 4' : 'traceroute -m 10'} ${domain}`, { timeout: 30000 });
res.send(result);
} catch (error) {
if (error.killed) {
return res.status(408).send('测试超时');
}
logger.error(`执行网络测试命令错误:`, error);
res.status(500).send('测试执行失败: ' + error.message);
}
});
// 获取用户统计信息
router.get('/user-stats', requireLogin, async (req, res) => {
try {
const userService = require('../services/userService');
const username = req.session.user.username;
const userStats = await userService.getUserStats(username);
res.json(userStats);
} catch (error) {
logger.error('获取用户统计信息失败:', error);
res.status(500).json({
loginCount: '0',
lastLogin: '未知',
accountAge: '0'
});
}
});
// 获取系统状态信息 (旧版,可能与 getSystemStats 重复,可以考虑移除)
router.get('/system-status', requireLogin, async (req, res) => {
logger.warn('Accessing potentially deprecated /api/system-status route.');
try {
// 检查 Docker 可用性
let dockerAvailable = true;
let containerCount = 0;
try {
// 避免直接执行命令计算,依赖 dockerService
const docker = await dockerService.getDockerConnection();
if (docker) {
const containers = await docker.listContainers({ all: true });
containerCount = containers.length;
} else {
dockerAvailable = false;
}
} catch (dockerError) {
dockerAvailable = false;
containerCount = 0;
logger.warn('Docker可能未运行或无法访问 (in /system-status):', dockerError.message);
}
// 获取内存使用信息
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`;
// 获取CPU负载
const [load1] = os.loadavg();
const cpuCount = os.cpus().length || 1; // 避免除以0
const cpuLoad = `${(load1 / cpuCount * 100).toFixed(1)}%`;
// 获取磁盘空间 - 简单版
let diskSpace = '未知';
try {
if (os.platform() === 'darwin' || os.platform() === 'linux') {
const { stdout } = await execPromise('df -h / | tail -n 1'); // 使用 -n 1
const parts = stdout.trim().split(/\s+/);
if (parts.length >= 5) diskSpace = parts[4];
} else if (os.platform() === 'win32') {
const { stdout } = await execPromise('wmic logicaldisk get size,freespace,caption | findstr /B /L /V "Caption" ');
const lines = stdout.trim().split(/\r?\n/);
if (lines.length > 0) {
const parts = lines[0].trim().split(/\s+/);
if (parts.length >= 2) {
const free = parseInt(parts[0], 10);
const total = parseInt(parts[1], 10);
if (!isNaN(total) && !isNaN(free) && total > 0) {
diskSpace = `${Math.round(((total - free) / total) * 100)}%`;
}
}
}
}
} catch (diskError) {
logger.warn('获取磁盘空间失败 (in /system-status):', diskError.message);
diskSpace = '未知';
}
// 格式化系统运行时间
const uptime = formatUptime(os.uptime());
res.json({
dockerAvailable,
containerCount,
memoryUsage,
cpuLoad,
diskSpace,
systemUptime: uptime
});
} catch (error) {
logger.error('获取系统状态失败 (in /system-status):', error);
res.status(500).json({
error: '获取系统状态失败',
message: error.message
});
}
});
// 添加新的API端点提供完整系统资源信息
router.get('/system-resources', requireLogin, async (req, res) => {
logger.info('Received request for /api/system-resources');
let cpuInfoData = null, memoryData = null, diskInfoData = null, systemData = null;
// --- 获取 CPU 信息 (独立 try...catch) ---
try {
const cpuInfo = os.cpus();
const [load1, load5, load15] = os.loadavg();
const cpuCount = cpuInfo.length || 1;
const cpuUsage = (load1 / cpuCount * 100).toFixed(1);
cpuInfoData = {
cores: cpuCount,
model: cpuInfo[0]?.model || '未知',
speed: `${cpuInfo[0]?.speed || '未知'} MHz`,
loadAvg: {
'1min': load1.toFixed(2),
'5min': load5.toFixed(2),
'15min': load15.toFixed(2)
},
usage: parseFloat(cpuUsage)
};
logger.info('Successfully retrieved CPU info.');
} catch (cpuError) {
logger.error('Error getting CPU info:', cpuError.message);
cpuInfoData = { error: '获取 CPU 信息失败', message: cpuError.message }; // 返回错误信息
}
// --- 获取内存信息 (独立 try...catch) ---
try {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
const memoryUsagePercent = totalMem > 0 ? Math.round(usedMem / totalMem * 100) : 0;
memoryData = {
total: formatBytes(totalMem), // 可能出错
free: formatBytes(freeMem), // 可能出错
used: formatBytes(usedMem), // 可能出错
usedPercentage: memoryUsagePercent
};
logger.info('Successfully retrieved Memory info.');
} catch (memError) {
logger.error('Error getting Memory info:', memError.message);
memoryData = { error: '获取内存信息失败', message: memError.message }; // 返回错误信息
}
// --- 获取磁盘信息 (独立 try...catch) ---
try {
let diskResult = { total: '未知', free: '未知', used: '未知', usedPercentage: '未知' };
logger.info(`Getting disk info for platform: ${os.platform()}`);
if (os.platform() === 'darwin' || os.platform() === 'linux') {
try {
// 使用 -k 获取 KB 单位,方便计算
const { stdout } = await execPromise('df -k / | tail -n 1', { timeout: 5000 });
logger.info(`'df -k' command output: ${stdout}`);
const parts = stdout.trim().split(/\s+/);
// 索引通常是 1=Total, 2=Used, 3=Available, 4=Use%
if (parts.length >= 4) {
const total = parseInt(parts[1], 10) * 1024; // KB to Bytes
const used = parseInt(parts[2], 10) * 1024; // KB to Bytes
const free = parseInt(parts[3], 10) * 1024; // KB to Bytes
// 优先使用命令输出的百分比,更准确
let usedPercentage = parseInt(parts[4].replace('%', ''), 10);
// 如果解析失败或百分比无效,则尝试计算
if (isNaN(usedPercentage) && !isNaN(total) && !isNaN(used) && total > 0) {
usedPercentage = Math.round((used / total) * 100);
}
if (!isNaN(total) && !isNaN(used) && !isNaN(free) && !isNaN(usedPercentage)) {
diskResult = {
total: formatBytes(total), // 可能出错
free: formatBytes(free), // 可能出错
used: formatBytes(used), // 可能出错
usedPercentage: usedPercentage
};
logger.info('Successfully parsed disk info (Linux/Darwin).');
} else {
logger.warn('Failed to parse numbers from df output:', parts);
diskResult = { ...diskResult, error: '解析 df 输出失败' }; // 添加错误标记
}
} else {
logger.warn('Unexpected output format from df:', stdout);
diskResult = { ...diskResult, error: 'df 输出格式不符合预期' }; // 添加错误标记
}
} catch (dfError) {
logger.error(`Error executing or parsing 'df -k': ${dfError.message}`);
if (dfError.killed) logger.error("'df -k' command timed out.");
diskResult = { error: '获取磁盘信息失败 (df)', message: dfError.message }; // 标记错误
}
} else if (os.platform() === 'win32') {
try {
// 获取 C 盘信息 (可以修改为获取所有盘符或特定盘符)
const { stdout } = await execPromise(`wmic logicaldisk where "DeviceID='C:'" get size,freespace /value`, { timeout: 5000 });
logger.info(`'wmic' command output: ${stdout}`);
const lines = stdout.trim().split(/\r?\n/);
let free = NaN, total = NaN;
lines.forEach(line => {
if (line.startsWith('FreeSpace=')) {
free = parseInt(line.split('=')[1], 10);
} else if (line.startsWith('Size=')) {
total = parseInt(line.split('=')[1], 10);
}
});
if (!isNaN(total) && !isNaN(free) && total > 0) {
const used = total - free;
const usedPercentage = Math.round((used / total) * 100);
diskResult = {
total: formatBytes(total), // 可能出错
free: formatBytes(free), // 可能出错
used: formatBytes(used), // 可能出错
usedPercentage: usedPercentage
};
logger.info('Successfully parsed disk info (Windows - C:).');
} else {
logger.warn('Failed to parse numbers from wmic output:', stdout);
diskResult = { ...diskResult, error: '解析 wmic 输出失败' }; // 添加错误标记
}
} catch (wmicError) {
logger.error(`Error executing or parsing 'wmic': ${wmicError.message}`);
if (wmicError.killed) logger.error("'wmic' command timed out.");
diskResult = { error: '获取磁盘信息失败 (wmic)', message: wmicError.message }; // 标记错误
}
}
diskInfoData = diskResult;
} catch (diskErrorOuter) {
logger.error('Unexpected error during disk info gathering:', diskErrorOuter.message);
diskInfoData = { error: '获取磁盘信息时发生意外错误', message: diskErrorOuter.message }; // 返回错误信息
}
// --- 获取其他系统信息 (独立 try...catch) ---
try {
systemData = {
platform: os.platform(),
release: os.release(),
hostname: os.hostname(),
uptime: formatUptime(os.uptime()) // 可能出错
};
logger.info('Successfully retrieved general system info.');
} catch (sysInfoError) {
logger.error('Error getting general system info:', sysInfoError.message);
systemData = { error: '获取常规系统信息失败', message: sysInfoError.message }; // 返回错误信息
}
// --- 包装 Helper 函数调用以捕获潜在错误 ---
const safeFormatBytes = (bytes) => {
try {
return formatBytes(bytes);
} catch (e) {
logger.error(`formatBytes failed for value ${bytes}:`, e.message);
return '格式化错误';
}
};
const safeFormatUptime = (seconds) => {
try {
return formatUptime(seconds);
} catch (e) {
logger.error(`formatUptime failed for value ${seconds}:`, e.message);
return '格式化错误';
}
};
// --- 构建最终响应数据,使用安全的 Helper 函数 ---
const finalCpuData = cpuInfoData?.error ? cpuInfoData : {
...cpuInfoData
// CPU 不需要格式化
};
const finalMemoryData = memoryData?.error ? memoryData : {
...memoryData,
total: safeFormatBytes(os.totalmem()),
free: safeFormatBytes(os.freemem()),
used: safeFormatBytes(os.totalmem() - os.freemem())
};
const finalDiskData = diskInfoData?.error ? diskInfoData : {
...diskInfoData,
// 如果 diskInfoData 内部有 total/free/used (字节数),则格式化
// 否则保持 '未知' 或已格式化的字符串
total: (diskInfoData?.total && typeof diskInfoData.total === 'number') ? safeFormatBytes(diskInfoData.total) : diskInfoData?.total || '未知',
free: (diskInfoData?.free && typeof diskInfoData.free === 'number') ? safeFormatBytes(diskInfoData.free) : diskInfoData?.free || '未知',
used: (diskInfoData?.used && typeof diskInfoData.used === 'number') ? safeFormatBytes(diskInfoData.used) : diskInfoData?.used || '未知'
};
const finalSystemData = systemData?.error ? systemData : {
...systemData,
uptime: safeFormatUptime(os.uptime())
};
const responseData = {
cpu: finalCpuData,
memory: finalMemoryData,
diskSpace: finalDiskData,
system: finalSystemData
};
logger.info('Sending response for /api/system-resources:', JSON.stringify(responseData));
res.status(200).json(responseData);
});
// 格式化系统运行时间
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
seconds %= 86400;
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
let result = '';
if (days > 0) result += `${days}`;
if (hours > 0 || days > 0) result += `${hours}小时 `;
if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}分钟 `;
result += `${seconds}`;
return result;
}
// 获取系统资源详情
router.get('/system-resource-details', requireLogin, async (req, res) => {
try {
const { type } = req.query;
let data = {};
switch (type) {
case 'memory':
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
data = {
totalMemory: formatBytes(totalMem),
usedMemory: formatBytes(usedMem),
freeMemory: formatBytes(freeMem),
memoryUsage: `${Math.round(usedMem / totalMem * 100)}%`
};
break;
case 'cpu':
const cpuInfo = os.cpus();
const [load1, load5, load15] = os.loadavg();
data = {
cpuCores: cpuInfo.length,
cpuModel: cpuInfo[0].model,
cpuSpeed: `${cpuInfo[0].speed} MHz`,
loadAvg1: load1.toFixed(2),
loadAvg5: load5.toFixed(2),
loadAvg15: load15.toFixed(2),
cpuLoad: `${(load1 / cpuInfo.length * 100).toFixed(1)}%`
};
break;
case 'disk':
try {
const { stdout: dfOutput } = await execPromise('df -h / | tail -n 1');
const parts = dfOutput.trim().split(/\s+/);
if (parts.length >= 5) {
data = {
totalSpace: parts[1],
usedSpace: parts[2],
freeSpace: parts[3],
diskUsage: parts[4]
};
} else {
throw new Error('解析磁盘信息失败');
}
} catch (diskError) {
logger.warn('获取磁盘信息失败:', diskError.message);
data = {
error: '获取磁盘信息失败',
message: diskError.message
};
}
break;
default:
return res.status(400).json({ error: '无效的资源类型' });
}
res.json(data);
} catch (error) {
logger.error('获取系统资源详情失败:', error);
res.status(500).json({ error: '获取系统资源详情失败', message: error.message });
}
});
// 格式化字节数为可读格式
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
module.exports = router; // 只导出 router