From 832d55c72235aa59bd0c647a71017344feaacd8f Mon Sep 17 00:00:00 2001 From: liumangmang Date: Mon, 30 Mar 2026 16:54:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E9=A1=B6=E9=83=A8=E5=AE=9E=E6=97=B6=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MonitorController.java | 139 +++++++++++ .../com/sshmanager/service/SshService.java | 63 ++++- frontend/src/components/TerminalWidget.vue | 225 ++++++++++++++---- 3 files changed, 370 insertions(+), 57 deletions(-) create mode 100644 backend/src/main/java/com/sshmanager/controller/MonitorController.java diff --git a/backend/src/main/java/com/sshmanager/controller/MonitorController.java b/backend/src/main/java/com/sshmanager/controller/MonitorController.java new file mode 100644 index 0000000..38b3028 --- /dev/null +++ b/backend/src/main/java/com/sshmanager/controller/MonitorController.java @@ -0,0 +1,139 @@ +package com.sshmanager.controller; + +import com.sshmanager.entity.Connection; +import com.sshmanager.entity.User; +import com.sshmanager.repository.UserRepository; +import com.sshmanager.service.ConnectionService; +import com.sshmanager.service.SshService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/monitor") +public class MonitorController { + + private final ConnectionService connectionService; + private final SshService sshService; + private final UserRepository userRepository; + + public MonitorController(ConnectionService connectionService, SshService sshService, UserRepository userRepository) { + this.connectionService = connectionService; + this.sshService = sshService; + this.userRepository = userRepository; + } + + private Long getCurrentUserId(Authentication auth) { + User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalStateException("User not found")); + return user.getId(); + } + + @GetMapping("/{connectionId}") + public ResponseEntity> getServerMetrics(@PathVariable Long connectionId, Authentication authentication) { + try { + Long userId = getCurrentUserId(authentication); + Connection conn = connectionService.getConnectionForSsh(connectionId, userId); + if (conn == null) { + return ResponseEntity.notFound().build(); + } + + String password = connectionService.getDecryptedPassword(conn); + String privateKey = connectionService.getDecryptedPrivateKey(conn); + String passphrase = connectionService.getDecryptedPassphrase(conn); + + Map metrics = new HashMap<>(); + + // 获取CPU使用率 + try { + String cpuOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' | awk '{print 100 - $1}'"); + double cpuUsage = Double.parseDouble(cpuOutput.trim()); + metrics.put("cpuUsage", Math.round(cpuUsage * 10.0) / 10.0); + } catch (Exception e) { + metrics.put("cpuUsage", null); + } + + // 获取内存信息 + try { + String memOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "free -b | grep Mem"); + String[] memParts = memOutput.trim().split("\\s+"); + long totalMem = Long.parseLong(memParts[1]); + long usedMem = Long.parseLong(memParts[2]); + double memUsage = (double) usedMem / totalMem * 100; + metrics.put("memTotal", totalMem); + metrics.put("memUsed", usedMem); + metrics.put("memUsage", Math.round(memUsage * 10.0) / 10.0); + } catch (Exception e) { + metrics.put("memTotal", null); + metrics.put("memUsed", null); + metrics.put("memUsage", null); + } + + // 获取磁盘使用率 + try { + String diskOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "df -P / | tail -1"); + String[] diskParts = diskOutput.trim().split("\\s+"); + int diskUsage = Integer.parseInt(diskParts[4].replace("%", "")); + metrics.put("diskUsage", diskUsage); + } catch (Exception e) { + metrics.put("diskUsage", null); + } + + // 获取系统负载 + try { + String loadOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "cat /proc/loadavg"); + String[] loadParts = loadOutput.trim().split("\\s+"); + double load1 = Double.parseDouble(loadParts[0]); + metrics.put("load1", load1); + } catch (Exception e) { + metrics.put("load1", null); + } + + // 获取CPU核数 + try { + String cpuCountOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "grep -c ^processor /proc/cpuinfo"); + int cpuCores = Integer.parseInt(cpuCountOutput.trim()); + metrics.put("cpuCores", cpuCores); + } catch (Exception e) { + metrics.put("cpuCores", null); + } + + // 获取系统运行时间 + try { + String uptimeOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "uptime -p"); + metrics.put("uptime", uptimeOutput.trim().replace("up ", "")); + } catch (Exception e) { + // 尝试另一种uptime格式 + try { + String uptimeOutput = sshService.executeCommand(conn, password, privateKey, passphrase, + "cat /proc/uptime | awk '{print $1}'"); + double uptimeSeconds = Double.parseDouble(uptimeOutput.trim()); + long days = (long) (uptimeSeconds / 86400); + long hours = (long) ((uptimeSeconds % 86400) / 3600); + long minutes = (long) ((uptimeSeconds % 3600) / 60); + StringBuilder uptimeStr = new StringBuilder(); + if (days > 0) uptimeStr.append(days).append("d "); + if (hours > 0) uptimeStr.append(hours).append("h "); + uptimeStr.append(minutes).append("m"); + metrics.put("uptime", uptimeStr.toString().trim()); + } catch (Exception e2) { + metrics.put("uptime", null); + } + } + + return ResponseEntity.ok(metrics); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } +} diff --git a/backend/src/main/java/com/sshmanager/service/SshService.java b/backend/src/main/java/com/sshmanager/service/SshService.java index b3b5177..f4519a7 100644 --- a/backend/src/main/java/com/sshmanager/service/SshService.java +++ b/backend/src/main/java/com/sshmanager/service/SshService.java @@ -1,22 +1,25 @@ package com.sshmanager.service; -import com.jcraft.jsch.ChannelShell; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.Session; -import com.sshmanager.entity.Connection; -import org.springframework.stereotype.Service; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import com.sshmanager.entity.Connection; +import org.springframework.stereotype.Service; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; -import java.io.PipedOutputStream; +import java.io.PipedOutputStream; @Service public class SshService { - public SshSession createShellSession(Connection conn, String password, String privateKey, String passphrase) - throws Exception { + public SshSession createShellSession(Connection conn, String password, String privateKey, String passphrase) + throws Exception { JSch jsch = new JSch(); if (conn.getAuthType() == Connection.AuthType.PRIVATE_KEY && privateKey != null && !privateKey.isEmpty()) { @@ -65,7 +68,49 @@ public class SshService { } }).start(); - return new SshSession(session, channel, channelOut, pipeToChannel); + return new SshSession(session, channel, channelOut, pipeToChannel); + } + + // 执行单次命令并返回输出 + public String executeCommand(Connection conn, String password, String privateKey, String passphrase, String command) throws Exception { + JSch jsch = new JSch(); + + if (conn.getAuthType() == Connection.AuthType.PRIVATE_KEY && privateKey != null && !privateKey.isEmpty()) { + byte[] keyBytes = privateKey.getBytes(StandardCharsets.UTF_8); + byte[] passphraseBytes = (passphrase != null && !passphrase.isEmpty()) + ? passphrase.getBytes(StandardCharsets.UTF_8) : null; + jsch.addIdentity("key", keyBytes, null, passphraseBytes); + } + + Session session = jsch.getSession(conn.getUsername(), conn.getHost(), conn.getPort()); + session.setConfig("StrictHostKeyChecking", "no"); + session.setConfig("kex", "diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1"); + session.setConfig("PreferredAuthentications", conn.getAuthType() == Connection.AuthType.PASSWORD ? "password" : "publickey"); + + if (conn.getAuthType() == Connection.AuthType.PASSWORD) { + session.setPassword(password); + } + + session.connect(8000); + + ChannelExec channel = (ChannelExec) session.openChannel("exec"); + channel.setCommand(command); + channel.setErrStream(System.err); + + InputStream in = channel.getInputStream(); + channel.connect(3000); + + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + StringBuilder result = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + result.append(line).append("\n"); + } + + channel.disconnect(); + session.disconnect(); + + return result.toString().trim(); } public static class SshSession { diff --git a/frontend/src/components/TerminalWidget.vue b/frontend/src/components/TerminalWidget.vue index 41ba406..36c8343 100644 --- a/frontend/src/components/TerminalWidget.vue +++ b/frontend/src/components/TerminalWidget.vue @@ -1,20 +1,35 @@ -