核心修复: - 后端 SyncSSH 接口彻底移除 hostname 字段更新,仅保留 os_version、cpu_info、 memory_info、disk_info、uptime、listen_ports 的系统信息同步。 - 前端 MachineDetail.vue 在 openSSH(自动获取)和 fetchSSH(手动输入密码) 两条路径均增加 delete syncData.hostname 保护,确保即使误传也不会覆盖。 - 重新编译后端二进制并重启服务,解决了旧二进制残留 hostname 更新逻辑的问题。 SSH 采集兼容性优化(server/services/ssh.go): - CPU:将复杂 shell 脚本改为 Go 层两次读取 /proc/stat 并计算利用率,避免 Debian 12 dash 对引号嵌套解析失败。 - 内存:改用 /proc/meminfo 替代 free -m,兼容最小化/容器环境。 - 磁盘:简化 df -hP 过滤,仅保留真实块设备( ~ /^/dev/)。 - 端口:增加 ss → netstat → /proc/net/tcp 三级回退,确保任何 Linux 环境 都能获取监听端口。 - 系统版本:采用 . /etc/os-release && printf '%s\n' 方式, 兼容所有 Linux 发行版。 构建与部署: - 重新构建前端并更新 server/static/。 - 交叉编译 Linux amd64 二进制并更新 deploy/lan-manager-debian12.tar.gz。 - 恢复数据库中被污染的主机名记录。
199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"lan-manager/server/config"
|
|
"lan-manager/server/models"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func GetSSHInfo(ip string, port int, user, pass string) (*models.SSHInfoResult, error) {
|
|
if port <= 0 {
|
|
port = 22
|
|
}
|
|
cfg := &ssh.ClientConfig{
|
|
User: user,
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password(pass),
|
|
},
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
Timeout: time.Duration(config.Load().SSHTimeout) * time.Second,
|
|
}
|
|
|
|
client, err := ssh.Dial("tcp", ip+":"+strconv.Itoa(port), cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer client.Close()
|
|
|
|
result := &models.SSHInfoResult{}
|
|
|
|
// Hostname
|
|
if out, err := runSSHCommand(client, "hostname"); err == nil {
|
|
result.Hostname = strings.TrimSpace(out)
|
|
}
|
|
|
|
// OS Version: source /etc/os-release is the most compatible way across Linux distros
|
|
if out, err := runSSHCommand(client, `. /etc/os-release 2>/dev/null && printf '%s\n' "$PRETTY_NAME"`); err == nil && out != "" {
|
|
result.OsVersion = strings.TrimSpace(out)
|
|
} else if out, err := runSSHCommand(client, "uname -sr"); err == nil {
|
|
result.OsVersion = strings.TrimSpace(out)
|
|
}
|
|
|
|
// CPU: read /proc/stat twice with 1s sleep in Go to avoid complex shell quoting
|
|
if cpuUsage, cores, err := getCPUInfo(client); err == nil {
|
|
result.CPU = cpuUsage + "%"
|
|
if cores != "" {
|
|
result.CPU += " (" + cores + " cores)"
|
|
}
|
|
result.RawCPUInfo = result.CPU
|
|
}
|
|
|
|
// Memory: use /proc/meminfo instead of free for better compatibility
|
|
if out, err := runSSHCommand(client, "awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} END{u=t-a; if(t>0) printf \"%.1f/%.1f (%.0f%%)\", u/1024/1024, t/1024/1024, u*100/t}' /proc/meminfo"); err == nil && out != "" {
|
|
result.Memory = strings.TrimSpace(out)
|
|
result.RawMemoryInfo = result.Memory
|
|
}
|
|
|
|
// Disk: keep real block devices only, exclude all virtual filesystems
|
|
if out, err := runSSHCommand(client, "df -hP | awk 'NR>1 && ($1 ~ /^\\/dev/ || $1 ~ /^(LABEL=|UUID=)/) {printf \"%s %s/%s (%s) \", $6,$3,$2,$5}'"); err == nil && out != "" {
|
|
result.Disk = strings.TrimSpace(out)
|
|
result.RawDiskInfo = result.Disk
|
|
}
|
|
|
|
// Uptime
|
|
if out, err := runSSHCommand(client, "uptime -p 2>/dev/null || uptime | awk -F',' '{print $1}'"); err == nil {
|
|
result.Uptime = strings.TrimSpace(out)
|
|
}
|
|
|
|
// Listen ports: try ss first, fallback to netstat, fallback to /proc/net/tcp
|
|
ports, _ := getListenPorts(client)
|
|
result.ListenPorts = ports
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func runSSHCommand(client *ssh.Client, cmd string) (string, error) {
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer session.Close()
|
|
var b bytes.Buffer
|
|
session.Stdout = &b
|
|
err = session.Run(cmd)
|
|
return b.String(), err
|
|
}
|
|
|
|
func getCPUInfo(client *ssh.Client) (usage string, cores string, err error) {
|
|
readStat := func() (idle, total float64, e error) {
|
|
out, e := runSSHCommand(client, "awk '/^cpu /{print $2,$3,$4,$5}' /proc/stat")
|
|
if e != nil {
|
|
return 0, 0, e
|
|
}
|
|
parts := strings.Fields(strings.TrimSpace(out))
|
|
if len(parts) < 4 {
|
|
return 0, 0, fmt.Errorf("unexpected /proc/stat format")
|
|
}
|
|
vals := make([]float64, 4)
|
|
for i := 0; i < 4; i++ {
|
|
v, e := strconv.ParseFloat(parts[i], 64)
|
|
if e != nil {
|
|
return 0, 0, e
|
|
}
|
|
vals[i] = v
|
|
}
|
|
user, nice, system, idleTick := vals[0], vals[1], vals[2], vals[3]
|
|
idle = idleTick
|
|
total = user + nice + system + idleTick
|
|
return idle, total, nil
|
|
}
|
|
|
|
idle1, total1, err := readStat()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
idle2, total2, err := readStat()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
dTotal := total2 - total1
|
|
dIdle := idle2 - idle1
|
|
if dTotal > 0 {
|
|
pct := 100.0 * (dTotal - dIdle) / dTotal
|
|
if pct < 0 {
|
|
pct = 0
|
|
}
|
|
if pct > 100 {
|
|
pct = 100
|
|
}
|
|
usage = fmt.Sprintf("%.1f", pct)
|
|
} else {
|
|
usage = "0.0"
|
|
}
|
|
|
|
if cout, e := runSSHCommand(client, "nproc"); e == nil {
|
|
cores = strings.TrimSpace(cout)
|
|
}
|
|
return usage, cores, nil
|
|
}
|
|
|
|
func getListenPorts(client *ssh.Client) ([]int, error) {
|
|
var out string
|
|
var err error
|
|
|
|
// try ss without -p (no need for process info, more compatible)
|
|
out, err = runSSHCommand(client, "ss -tln 2>/dev/null | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -n | uniq")
|
|
if err != nil || strings.TrimSpace(out) == "" {
|
|
// fallback to netstat
|
|
out, err = runSSHCommand(client, "netstat -tln 2>/dev/null | awk 'NR>2 {print $4}' | sed 's/.*://' | sort -n | uniq")
|
|
}
|
|
if err != nil || strings.TrimSpace(out) == "" {
|
|
// fallback to /proc/net/tcp
|
|
out, err = runSSHCommand(client, "awk 'NR>1 {printf \"%04X\\n\", strtonum(\"0x\"$2)}' /proc/net/tcp")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ports := []int{}
|
|
scanner := bufio.NewScanner(strings.NewReader(out))
|
|
for scanner.Scan() {
|
|
p := strings.TrimSpace(scanner.Text())
|
|
if p == "" {
|
|
continue
|
|
}
|
|
// handle both decimal ports and hex ports from /proc/net/tcp
|
|
var pi int
|
|
var e error
|
|
if len(p) == 4 && isHex(p) {
|
|
var h uint64
|
|
h, e = strconv.ParseUint(p, 16, 32)
|
|
pi = int(h)
|
|
} else {
|
|
pi, e = strconv.Atoi(p)
|
|
}
|
|
if e == nil && pi > 0 {
|
|
ports = append(ports, pi)
|
|
}
|
|
}
|
|
return ports, nil
|
|
}
|
|
|
|
func isHex(s string) bool {
|
|
for _, c := range s {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|