system.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. /**
  2. * 系统相关路由
  3. */
  4. const express = require('express');
  5. const router = express.Router();
  6. const os = require('os'); // 确保导入 os 模块
  7. const util = require('util'); // 导入 util 模块
  8. const { exec } = require('child_process');
  9. const execPromise = util.promisify(exec); // 只在这里定义一次
  10. const logger = require('../logger');
  11. const { requireLogin } = require('../middleware/auth');
  12. const configService = require('../services/configService');
  13. const { execCommand, getSystemInfo } = require('../server-utils');
  14. const dockerService = require('../services/dockerService');
  15. const path = require('path');
  16. const fs = require('fs').promises;
  17. // 获取系统状态
  18. async function getSystemStats(req, res) {
  19. try {
  20. let dockerAvailable = false;
  21. let containerCount = '0';
  22. let memoryUsage = '0%';
  23. let cpuLoad = '0%';
  24. let diskSpace = '0%';
  25. let recentActivities = [];
  26. // 尝试获取系统信息
  27. try {
  28. const systemInfo = await getSystemInfo();
  29. memoryUsage = `${systemInfo.memory.percent}%`;
  30. cpuLoad = systemInfo.cpu.load1;
  31. diskSpace = systemInfo.disk.percent;
  32. } catch (sysError) {
  33. logger.error('获取系统信息失败:', sysError);
  34. }
  35. // 尝试从Docker获取状态信息
  36. try {
  37. const docker = await dockerService.getDockerConnection();
  38. if (docker) {
  39. dockerAvailable = true;
  40. // 获取容器统计
  41. const containers = await docker.listContainers({ all: true });
  42. containerCount = containers.length.toString();
  43. // 获取最近的容器活动
  44. const runningContainers = containers.filter(c => c.State === 'running');
  45. for (let i = 0; i < Math.min(3, runningContainers.length); i++) {
  46. recentActivities.push({
  47. time: new Date(runningContainers[i].Created * 1000).toLocaleString(),
  48. action: '运行中',
  49. container: runningContainers[i].Names[0].replace(/^\//, ''),
  50. status: '正常'
  51. });
  52. }
  53. // 获取最近的Docker事件
  54. const events = await dockerService.getRecentEvents();
  55. if (events && events.length > 0) {
  56. recentActivities = [...events.map(event => ({
  57. time: new Date(event.time * 1000).toLocaleString(),
  58. action: event.Action,
  59. container: event.Actor?.Attributes?.name || '未知容器',
  60. status: event.status || '完成'
  61. })), ...recentActivities].slice(0, 10);
  62. }
  63. }
  64. } catch (containerError) {
  65. logger.error('获取容器信息失败:', containerError);
  66. }
  67. // 如果没有活动记录,添加一个默认记录
  68. if (recentActivities.length === 0) {
  69. recentActivities.push({
  70. time: new Date().toLocaleString(),
  71. action: '系统检查',
  72. container: '监控服务',
  73. status: dockerAvailable ? '正常' : 'Docker服务不可用'
  74. });
  75. }
  76. // 返回收集到的所有数据,即使部分数据可能不完整
  77. res.json({
  78. dockerAvailable,
  79. containerCount,
  80. memoryUsage,
  81. cpuLoad,
  82. diskSpace,
  83. recentActivities
  84. });
  85. } catch (error) {
  86. logger.error('获取系统统计数据失败:', error);
  87. // 即使出错,仍然尝试返回一些基本数据
  88. res.status(200).json({
  89. dockerAvailable: false,
  90. containerCount: '0',
  91. memoryUsage: '未知',
  92. cpuLoad: '未知',
  93. diskSpace: '未知',
  94. recentActivities: [{
  95. time: new Date().toLocaleString(),
  96. action: '系统错误',
  97. container: '监控服务',
  98. status: '数据获取失败'
  99. }],
  100. error: '获取系统统计数据失败',
  101. errorDetails: error.message
  102. });
  103. }
  104. }
  105. // 获取系统配置 - 修改版本,避免与其他路由冲突
  106. router.get('/system-config', async (req, res) => {
  107. try {
  108. const config = await configService.getConfig();
  109. res.json(config);
  110. } catch (error) {
  111. logger.error('读取配置失败:', error);
  112. res.status(500).json({
  113. error: '读取配置失败',
  114. details: error.message
  115. });
  116. }
  117. });
  118. // 保存系统配置 - 修改版本,避免与其他路由冲突
  119. router.post('/system-config', requireLogin, async (req, res) => {
  120. try {
  121. const currentConfig = await configService.getConfig();
  122. const newConfig = { ...currentConfig, ...req.body };
  123. await configService.saveConfig(newConfig);
  124. logger.info('系统配置已更新');
  125. res.json({ success: true });
  126. } catch (error) {
  127. logger.error('保存配置失败:', error);
  128. res.status(500).json({
  129. error: '保存配置失败',
  130. details: error.message
  131. });
  132. }
  133. });
  134. // 获取系统状态
  135. router.get('/stats', requireLogin, async (req, res) => {
  136. return await getSystemStats(req, res);
  137. });
  138. // 获取磁盘空间信息
  139. router.get('/disk-space', requireLogin, async (req, res) => {
  140. try {
  141. const systemInfo = await getSystemInfo();
  142. res.json({
  143. diskSpace: `${systemInfo.disk.used}/${systemInfo.disk.size}`,
  144. usagePercent: parseInt(systemInfo.disk.percent)
  145. });
  146. } catch (error) {
  147. logger.error('获取磁盘空间信息失败:', error);
  148. res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message });
  149. }
  150. });
  151. // 网络测试
  152. router.post('/network-test', requireLogin, async (req, res) => {
  153. const { type, domain } = req.body;
  154. // 验证输入
  155. function validateInput(input, type) {
  156. if (type === 'domain') {
  157. return /^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(input);
  158. }
  159. return false;
  160. }
  161. if (!validateInput(domain, 'domain')) {
  162. return res.status(400).json({ error: '无效的域名格式' });
  163. }
  164. try {
  165. const result = await execCommand(`${type === 'ping' ? 'ping -c 4' : 'traceroute -m 10'} ${domain}`, { timeout: 30000 });
  166. res.send(result);
  167. } catch (error) {
  168. if (error.killed) {
  169. return res.status(408).send('测试超时');
  170. }
  171. logger.error(`执行网络测试命令错误:`, error);
  172. res.status(500).send('测试执行失败: ' + error.message);
  173. }
  174. });
  175. // 获取用户统计信息
  176. router.get('/user-stats', requireLogin, async (req, res) => {
  177. try {
  178. const userService = require('../services/userService');
  179. const username = req.session.user.username;
  180. const userStats = await userService.getUserStats(username);
  181. res.json(userStats);
  182. } catch (error) {
  183. logger.error('获取用户统计信息失败:', error);
  184. res.status(500).json({
  185. loginCount: '0',
  186. lastLogin: '未知',
  187. accountAge: '0'
  188. });
  189. }
  190. });
  191. // 获取系统状态信息 (旧版,可能与 getSystemStats 重复,可以考虑移除)
  192. router.get('/system-status', requireLogin, async (req, res) => {
  193. logger.warn('Accessing potentially deprecated /api/system-status route.');
  194. try {
  195. // 检查 Docker 可用性
  196. let dockerAvailable = true;
  197. let containerCount = 0;
  198. try {
  199. // 避免直接执行命令计算,依赖 dockerService
  200. const docker = await dockerService.getDockerConnection();
  201. if (docker) {
  202. const containers = await docker.listContainers({ all: true });
  203. containerCount = containers.length;
  204. } else {
  205. dockerAvailable = false;
  206. }
  207. } catch (dockerError) {
  208. dockerAvailable = false;
  209. containerCount = 0;
  210. logger.warn('Docker可能未运行或无法访问 (in /system-status):', dockerError.message);
  211. }
  212. // 获取内存使用信息
  213. const totalMem = os.totalmem();
  214. const freeMem = os.freemem();
  215. const usedMem = totalMem - freeMem;
  216. const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`;
  217. // 获取CPU负载
  218. const [load1] = os.loadavg();
  219. const cpuCount = os.cpus().length || 1; // 避免除以0
  220. const cpuLoad = `${(load1 / cpuCount * 100).toFixed(1)}%`;
  221. // 获取磁盘空间 - 简单版
  222. let diskSpace = '未知';
  223. try {
  224. if (os.platform() === 'darwin' || os.platform() === 'linux') {
  225. const { stdout } = await execPromise('df -h / | tail -n 1'); // 使用 -n 1
  226. const parts = stdout.trim().split(/\s+/);
  227. if (parts.length >= 5) diskSpace = parts[4];
  228. } else if (os.platform() === 'win32') {
  229. const { stdout } = await execPromise('wmic logicaldisk get size,freespace,caption | findstr /B /L /V "Caption" ');
  230. const lines = stdout.trim().split(/\r?\n/);
  231. if (lines.length > 0) {
  232. const parts = lines[0].trim().split(/\s+/);
  233. if (parts.length >= 2) {
  234. const free = parseInt(parts[0], 10);
  235. const total = parseInt(parts[1], 10);
  236. if (!isNaN(total) && !isNaN(free) && total > 0) {
  237. diskSpace = `${Math.round(((total - free) / total) * 100)}%`;
  238. }
  239. }
  240. }
  241. }
  242. } catch (diskError) {
  243. logger.warn('获取磁盘空间失败 (in /system-status):', diskError.message);
  244. diskSpace = '未知';
  245. }
  246. // 格式化系统运行时间
  247. const uptime = formatUptime(os.uptime());
  248. res.json({
  249. dockerAvailable,
  250. containerCount,
  251. memoryUsage,
  252. cpuLoad,
  253. diskSpace,
  254. systemUptime: uptime
  255. });
  256. } catch (error) {
  257. logger.error('获取系统状态失败 (in /system-status):', error);
  258. res.status(500).json({
  259. error: '获取系统状态失败',
  260. message: error.message
  261. });
  262. }
  263. });
  264. // 添加新的API端点,提供完整系统资源信息
  265. router.get('/system-resources', requireLogin, async (req, res) => {
  266. logger.info('Received request for /api/system-resources');
  267. let cpuInfoData = null, memoryData = null, diskInfoData = null, systemData = null;
  268. // --- 获取 CPU 信息 (独立 try...catch) ---
  269. try {
  270. const cpuInfo = os.cpus();
  271. const [load1, load5, load15] = os.loadavg();
  272. const cpuCount = cpuInfo.length || 1;
  273. const cpuUsage = (load1 / cpuCount * 100).toFixed(1);
  274. cpuInfoData = {
  275. cores: cpuCount,
  276. model: cpuInfo[0]?.model || '未知',
  277. speed: `${cpuInfo[0]?.speed || '未知'} MHz`,
  278. loadAvg: {
  279. '1min': load1.toFixed(2),
  280. '5min': load5.toFixed(2),
  281. '15min': load15.toFixed(2)
  282. },
  283. usage: parseFloat(cpuUsage)
  284. };
  285. logger.info('Successfully retrieved CPU info.');
  286. } catch (cpuError) {
  287. logger.error('Error getting CPU info:', cpuError.message);
  288. cpuInfoData = { error: '获取 CPU 信息失败', message: cpuError.message }; // 返回错误信息
  289. }
  290. // --- 获取内存信息 (独立 try...catch) ---
  291. try {
  292. const totalMem = os.totalmem();
  293. const freeMem = os.freemem();
  294. const usedMem = totalMem - freeMem;
  295. const memoryUsagePercent = totalMem > 0 ? Math.round(usedMem / totalMem * 100) : 0;
  296. memoryData = {
  297. total: formatBytes(totalMem), // 可能出错
  298. free: formatBytes(freeMem), // 可能出错
  299. used: formatBytes(usedMem), // 可能出错
  300. usedPercentage: memoryUsagePercent
  301. };
  302. logger.info('Successfully retrieved Memory info.');
  303. } catch (memError) {
  304. logger.error('Error getting Memory info:', memError.message);
  305. memoryData = { error: '获取内存信息失败', message: memError.message }; // 返回错误信息
  306. }
  307. // --- 获取磁盘信息 (独立 try...catch) ---
  308. try {
  309. let diskResult = { total: '未知', free: '未知', used: '未知', usedPercentage: '未知' };
  310. logger.info(`Getting disk info for platform: ${os.platform()}`);
  311. if (os.platform() === 'darwin' || os.platform() === 'linux') {
  312. try {
  313. // 使用 -k 获取 KB 单位,方便计算
  314. const { stdout } = await execPromise('df -k / | tail -n 1', { timeout: 5000 });
  315. logger.info(`'df -k' command output: ${stdout}`);
  316. const parts = stdout.trim().split(/\s+/);
  317. // 索引通常是 1=Total, 2=Used, 3=Available, 4=Use%
  318. if (parts.length >= 4) {
  319. const total = parseInt(parts[1], 10) * 1024; // KB to Bytes
  320. const used = parseInt(parts[2], 10) * 1024; // KB to Bytes
  321. const free = parseInt(parts[3], 10) * 1024; // KB to Bytes
  322. // 优先使用命令输出的百分比,更准确
  323. let usedPercentage = parseInt(parts[4].replace('%', ''), 10);
  324. // 如果解析失败或百分比无效,则尝试计算
  325. if (isNaN(usedPercentage) && !isNaN(total) && !isNaN(used) && total > 0) {
  326. usedPercentage = Math.round((used / total) * 100);
  327. }
  328. if (!isNaN(total) && !isNaN(used) && !isNaN(free) && !isNaN(usedPercentage)) {
  329. diskResult = {
  330. total: formatBytes(total), // 可能出错
  331. free: formatBytes(free), // 可能出错
  332. used: formatBytes(used), // 可能出错
  333. usedPercentage: usedPercentage
  334. };
  335. logger.info('Successfully parsed disk info (Linux/Darwin).');
  336. } else {
  337. logger.warn('Failed to parse numbers from df output:', parts);
  338. diskResult = { ...diskResult, error: '解析 df 输出失败' }; // 添加错误标记
  339. }
  340. } else {
  341. logger.warn('Unexpected output format from df:', stdout);
  342. diskResult = { ...diskResult, error: 'df 输出格式不符合预期' }; // 添加错误标记
  343. }
  344. } catch (dfError) {
  345. logger.error(`Error executing or parsing 'df -k': ${dfError.message}`);
  346. if (dfError.killed) logger.error("'df -k' command timed out.");
  347. diskResult = { error: '获取磁盘信息失败 (df)', message: dfError.message }; // 标记错误
  348. }
  349. } else if (os.platform() === 'win32') {
  350. try {
  351. // 获取 C 盘信息 (可以修改为获取所有盘符或特定盘符)
  352. const { stdout } = await execPromise(`wmic logicaldisk where "DeviceID='C:'" get size,freespace /value`, { timeout: 5000 });
  353. logger.info(`'wmic' command output: ${stdout}`);
  354. const lines = stdout.trim().split(/\r?\n/);
  355. let free = NaN, total = NaN;
  356. lines.forEach(line => {
  357. if (line.startsWith('FreeSpace=')) {
  358. free = parseInt(line.split('=')[1], 10);
  359. } else if (line.startsWith('Size=')) {
  360. total = parseInt(line.split('=')[1], 10);
  361. }
  362. });
  363. if (!isNaN(total) && !isNaN(free) && total > 0) {
  364. const used = total - free;
  365. const usedPercentage = Math.round((used / total) * 100);
  366. diskResult = {
  367. total: formatBytes(total), // 可能出错
  368. free: formatBytes(free), // 可能出错
  369. used: formatBytes(used), // 可能出错
  370. usedPercentage: usedPercentage
  371. };
  372. logger.info('Successfully parsed disk info (Windows - C:).');
  373. } else {
  374. logger.warn('Failed to parse numbers from wmic output:', stdout);
  375. diskResult = { ...diskResult, error: '解析 wmic 输出失败' }; // 添加错误标记
  376. }
  377. } catch (wmicError) {
  378. logger.error(`Error executing or parsing 'wmic': ${wmicError.message}`);
  379. if (wmicError.killed) logger.error("'wmic' command timed out.");
  380. diskResult = { error: '获取磁盘信息失败 (wmic)', message: wmicError.message }; // 标记错误
  381. }
  382. }
  383. diskInfoData = diskResult;
  384. } catch (diskErrorOuter) {
  385. logger.error('Unexpected error during disk info gathering:', diskErrorOuter.message);
  386. diskInfoData = { error: '获取磁盘信息时发生意外错误', message: diskErrorOuter.message }; // 返回错误信息
  387. }
  388. // --- 获取其他系统信息 (独立 try...catch) ---
  389. try {
  390. systemData = {
  391. platform: os.platform(),
  392. release: os.release(),
  393. hostname: os.hostname(),
  394. uptime: formatUptime(os.uptime()) // 可能出错
  395. };
  396. logger.info('Successfully retrieved general system info.');
  397. } catch (sysInfoError) {
  398. logger.error('Error getting general system info:', sysInfoError.message);
  399. systemData = { error: '获取常规系统信息失败', message: sysInfoError.message }; // 返回错误信息
  400. }
  401. // --- 包装 Helper 函数调用以捕获潜在错误 ---
  402. const safeFormatBytes = (bytes) => {
  403. try {
  404. return formatBytes(bytes);
  405. } catch (e) {
  406. logger.error(`formatBytes failed for value ${bytes}:`, e.message);
  407. return '格式化错误';
  408. }
  409. };
  410. const safeFormatUptime = (seconds) => {
  411. try {
  412. return formatUptime(seconds);
  413. } catch (e) {
  414. logger.error(`formatUptime failed for value ${seconds}:`, e.message);
  415. return '格式化错误';
  416. }
  417. };
  418. // --- 构建最终响应数据,使用安全的 Helper 函数 ---
  419. const finalCpuData = cpuInfoData?.error ? cpuInfoData : {
  420. ...cpuInfoData
  421. // CPU 不需要格式化
  422. };
  423. const finalMemoryData = memoryData?.error ? memoryData : {
  424. ...memoryData,
  425. total: safeFormatBytes(os.totalmem()),
  426. free: safeFormatBytes(os.freemem()),
  427. used: safeFormatBytes(os.totalmem() - os.freemem())
  428. };
  429. const finalDiskData = diskInfoData?.error ? diskInfoData : {
  430. ...diskInfoData,
  431. // 如果 diskInfoData 内部有 total/free/used (字节数),则格式化
  432. // 否则保持 '未知' 或已格式化的字符串
  433. total: (diskInfoData?.total && typeof diskInfoData.total === 'number') ? safeFormatBytes(diskInfoData.total) : diskInfoData?.total || '未知',
  434. free: (diskInfoData?.free && typeof diskInfoData.free === 'number') ? safeFormatBytes(diskInfoData.free) : diskInfoData?.free || '未知',
  435. used: (diskInfoData?.used && typeof diskInfoData.used === 'number') ? safeFormatBytes(diskInfoData.used) : diskInfoData?.used || '未知'
  436. };
  437. const finalSystemData = systemData?.error ? systemData : {
  438. ...systemData,
  439. uptime: safeFormatUptime(os.uptime())
  440. };
  441. const responseData = {
  442. cpu: finalCpuData,
  443. memory: finalMemoryData,
  444. diskSpace: finalDiskData,
  445. system: finalSystemData
  446. };
  447. logger.info('Sending response for /api/system-resources:', JSON.stringify(responseData));
  448. res.status(200).json(responseData);
  449. });
  450. // 格式化系统运行时间
  451. function formatUptime(seconds) {
  452. const days = Math.floor(seconds / 86400);
  453. seconds %= 86400;
  454. const hours = Math.floor(seconds / 3600);
  455. seconds %= 3600;
  456. const minutes = Math.floor(seconds / 60);
  457. seconds = Math.floor(seconds % 60);
  458. let result = '';
  459. if (days > 0) result += `${days}天 `;
  460. if (hours > 0 || days > 0) result += `${hours}小时 `;
  461. if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}分钟 `;
  462. result += `${seconds}秒`;
  463. return result;
  464. }
  465. // 获取系统资源详情
  466. router.get('/system-resource-details', requireLogin, async (req, res) => {
  467. try {
  468. const { type } = req.query;
  469. let data = {};
  470. switch (type) {
  471. case 'memory':
  472. const totalMem = os.totalmem();
  473. const freeMem = os.freemem();
  474. const usedMem = totalMem - freeMem;
  475. data = {
  476. totalMemory: formatBytes(totalMem),
  477. usedMemory: formatBytes(usedMem),
  478. freeMemory: formatBytes(freeMem),
  479. memoryUsage: `${Math.round(usedMem / totalMem * 100)}%`
  480. };
  481. break;
  482. case 'cpu':
  483. const cpuInfo = os.cpus();
  484. const [load1, load5, load15] = os.loadavg();
  485. data = {
  486. cpuCores: cpuInfo.length,
  487. cpuModel: cpuInfo[0].model,
  488. cpuSpeed: `${cpuInfo[0].speed} MHz`,
  489. loadAvg1: load1.toFixed(2),
  490. loadAvg5: load5.toFixed(2),
  491. loadAvg15: load15.toFixed(2),
  492. cpuLoad: `${(load1 / cpuInfo.length * 100).toFixed(1)}%`
  493. };
  494. break;
  495. case 'disk':
  496. try {
  497. const { stdout: dfOutput } = await execPromise('df -h / | tail -n 1');
  498. const parts = dfOutput.trim().split(/\s+/);
  499. if (parts.length >= 5) {
  500. data = {
  501. totalSpace: parts[1],
  502. usedSpace: parts[2],
  503. freeSpace: parts[3],
  504. diskUsage: parts[4]
  505. };
  506. } else {
  507. throw new Error('解析磁盘信息失败');
  508. }
  509. } catch (diskError) {
  510. logger.warn('获取磁盘信息失败:', diskError.message);
  511. data = {
  512. error: '获取磁盘信息失败',
  513. message: diskError.message
  514. };
  515. }
  516. break;
  517. default:
  518. return res.status(400).json({ error: '无效的资源类型' });
  519. }
  520. res.json(data);
  521. } catch (error) {
  522. logger.error('获取系统资源详情失败:', error);
  523. res.status(500).json({ error: '获取系统资源详情失败', message: error.message });
  524. }
  525. });
  526. // 格式化字节数为可读格式
  527. function formatBytes(bytes, decimals = 2) {
  528. if (bytes === 0) return '0 Bytes';
  529. const k = 1024;
  530. const dm = decimals < 0 ? 0 : decimals;
  531. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  532. const i = Math.floor(Math.log(bytes) / Math.log(k));
  533. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  534. }
  535. module.exports = router; // 只导出 router