Compare commits
16 Commits
e06b0c1fcd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f2845f6 | |||
| 2f6bea41d5 | |||
| 5ef7937d3c | |||
| b412b8f9d1 | |||
| 9bdc92252e | |||
| 62143a5a32 | |||
| 2c103af9d8 | |||
| d39713a196 | |||
| 8f1325800c | |||
| 2377530fab | |||
| 6605adaed5 | |||
| 7ff0973a5b | |||
| 6ebad2a065 | |||
| 7217974395 | |||
| 65310c07e9 | |||
| 33d765b124 |
+1
-1
@@ -8,6 +8,6 @@
|
||||
# 删除此文件可重新配置
|
||||
|
||||
machine_name: my-pc
|
||||
server: 192.168.1.100:8080
|
||||
server: 127.0.0.1:8080
|
||||
interval: 3
|
||||
reconnect_delay: 3
|
||||
|
||||
+419
-175
@@ -134,15 +134,25 @@ type MemInfo struct {
|
||||
Percent float64 `json:"percent"`
|
||||
}
|
||||
type GPUInfo struct {
|
||||
Name string `json:"name"`
|
||||
VRAMTotalMB float64 `json:"vram_total_mb"`
|
||||
VRAMUsedMB float64 `json:"vram_used_mb"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
LoadPercent float64 `json:"load_percent"`
|
||||
CoreClock float64 `json:"core_clock_mhz"`
|
||||
MemClock float64 `json:"mem_clock_mhz"`
|
||||
Power float64 `json:"power_watts"`
|
||||
FanSpeed float64 `json:"fan_speed"`
|
||||
Name string `json:"name"`
|
||||
VRAMTotalMB float64 `json:"vram_total_mb"`
|
||||
VRAMUsedMB float64 `json:"vram_used_mb"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
LoadPercent float64 `json:"load_percent"`
|
||||
CoreClock float64 `json:"core_clock_mhz"`
|
||||
MemClock float64 `json:"mem_clock_mhz"`
|
||||
Power float64 `json:"power"`
|
||||
PowerLimit float64 `json:"power_limit"`
|
||||
PowerMax float64 `json:"power_max"`
|
||||
FanSpeed float64 `json:"fan_speed"`
|
||||
PState string `json:"pstate"`
|
||||
ComputeMode string `json:"compute_mode"`
|
||||
DriverVersion string `json:"driver_version"`
|
||||
UtilMemory float64 `json:"util_memory"`
|
||||
UtilEncoder float64 `json:"util_encoder"`
|
||||
UtilDecoder float64 `json:"util_decoder"`
|
||||
ClocksMaxGfx float64 `json:"clocks_max_graphics"`
|
||||
ClocksMaxMem float64 `json:"clocks_max_memory"`
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
@@ -543,6 +553,8 @@ func (m model) viewMonitor() string {
|
||||
if tw < 60 {
|
||||
tw = 60
|
||||
}
|
||||
gap := 2
|
||||
halfW := (tw - gap) / 2
|
||||
|
||||
// ── 顶栏 ──
|
||||
wsDot := "○"
|
||||
@@ -562,47 +574,33 @@ func (m model) viewMonitor() string {
|
||||
)
|
||||
sep := stDim.Render(strings.Repeat("─", tw))
|
||||
|
||||
// ── 列宽计算 ──
|
||||
gap := 2
|
||||
var leftW, rightW int
|
||||
if tw >= 100 {
|
||||
leftW = (tw - gap) / 2
|
||||
rightW = tw - gap - leftW
|
||||
} else {
|
||||
leftW = tw
|
||||
rightW = tw
|
||||
gap = 0
|
||||
}
|
||||
innerL := leftW - 4
|
||||
innerR := rightW - 4
|
||||
// ── 第一行: CPU + Memory ──
|
||||
cpuBox := m.viewCPUBox(halfW - 4)
|
||||
memBox := m.viewMemBox(halfW - 4)
|
||||
row1 := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
cpuBox, strings.Repeat(" ", gap), memBox,
|
||||
)
|
||||
|
||||
// ── 左列: CPU + History ──
|
||||
cpuBox := m.viewCPUBox(innerL)
|
||||
histBox := m.viewHistBox(innerL)
|
||||
leftCol := lipgloss.JoinVertical(lipgloss.Left, cpuBox, "", histBox)
|
||||
|
||||
// ── 右列: Memory + GPU ──
|
||||
memBox := m.viewMemBox(innerR)
|
||||
gpuBox := m.viewGPUBox(innerR)
|
||||
rightCol := lipgloss.JoinVertical(lipgloss.Left, memBox, "", gpuBox)
|
||||
|
||||
// ── 合并列 ──
|
||||
var mainContent string
|
||||
if gap > 0 {
|
||||
mainContent = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
leftCol, strings.Repeat(" ", gap), rightCol,
|
||||
)
|
||||
} else {
|
||||
mainContent = lipgloss.JoinVertical(lipgloss.Left, leftCol, "", rightCol)
|
||||
}
|
||||
|
||||
// ── 底栏 ──
|
||||
footer := stDim.Render(" q 退出 · r 重新配置 · 连接: " + m.cfg.Server)
|
||||
// ── 第二行: GPU + History ──
|
||||
gpuBox := m.viewGPUBox(halfW - 4)
|
||||
histBox := m.viewHistBox(halfW - 4)
|
||||
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
gpuBox, strings.Repeat(" ", gap), histBox,
|
||||
)
|
||||
|
||||
full := lipgloss.JoinVertical(lipgloss.Left,
|
||||
header, sep, "", mainContent, "", sep, footer,
|
||||
header, sep, "",
|
||||
row1, "",
|
||||
row2, "",
|
||||
sep,
|
||||
stDim.Render(" q 退出 · r 重新配置 · 连接: "+m.cfg.Server),
|
||||
)
|
||||
return full
|
||||
|
||||
lines := strings.Split(full, "\n")
|
||||
if len(lines) > m.h {
|
||||
lines = lines[:m.h]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// ── CPU 盒子 ──
|
||||
@@ -611,40 +609,38 @@ func (m model) viewCPUBox(innerW int) string {
|
||||
c := m.cpu
|
||||
pct := c.UsageTotal
|
||||
|
||||
// 标题
|
||||
modelName := c.Model
|
||||
maxNameW := innerW - 10
|
||||
maxNameW := innerW - 8
|
||||
if len(modelName) > maxNameW && maxNameW > 3 {
|
||||
modelName = modelName[:maxNameW-1] + "…"
|
||||
}
|
||||
titleLine := stTitle.Render("CPU") + " " + stFg.Render(modelName)
|
||||
|
||||
// 百分比进度条
|
||||
// 标题 + 核心/频率/温度 一行
|
||||
coresStr := fmt.Sprintf("%dC/%dT", c.CoresPhysical, c.CoresLogical)
|
||||
freqStr := fmt.Sprintf("%.0fMHz", c.FreqMHz)
|
||||
tempStr := tempText(c.Temperature)
|
||||
titleLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stTitle.Render("CPU"),
|
||||
" ", stFg.Render(modelName),
|
||||
" ", stLabel.Render(coresStr),
|
||||
" ", stVal.Render(freqStr),
|
||||
" ", tempStyle(c.Temperature).Render(tempStr),
|
||||
)
|
||||
|
||||
// 进度条 + 百分比 一行
|
||||
barW := innerW - 8
|
||||
if barW < 10 {
|
||||
barW = 10
|
||||
}
|
||||
bar := renderBar(pct, barW)
|
||||
pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%5.1f%%", pct))
|
||||
pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct))
|
||||
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr)
|
||||
|
||||
// 信息行
|
||||
coresStr := fmt.Sprintf("%dC/%dT", c.CoresPhysical, c.CoresLogical)
|
||||
freqStr := fmt.Sprintf("%.0f MHz", c.FreqMHz)
|
||||
tempStr := tempText(c.Temperature)
|
||||
infoLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stLabel.Render("核心 "), stVal.Render(coresStr),
|
||||
" ",
|
||||
stLabel.Render("频率 "), stVal.Render(freqStr),
|
||||
" ",
|
||||
stLabel.Render("温度 "), tempStyle(c.Temperature).Render(tempStr),
|
||||
)
|
||||
|
||||
// 核心热力图
|
||||
coreLine := m.viewCoreHeatmap(innerW)
|
||||
|
||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleLine, "", barLine, infoLine, "", coreLine,
|
||||
titleLine, barLine, coreLine,
|
||||
)
|
||||
return stBox.Width(innerW + 4).Render(body)
|
||||
}
|
||||
@@ -683,10 +679,16 @@ func (m model) viewCoreHeatmap(innerW int) string {
|
||||
func (m model) viewMemBox(innerW int) string {
|
||||
pct := m.mem.Percent
|
||||
|
||||
titleLine := stTitle.Render("MEMORY")
|
||||
usedGB := fmtBytes(m.mem.UsedBytes)
|
||||
totalGB := fmtBytes(m.mem.TotalBytes)
|
||||
|
||||
// 百分比
|
||||
pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct))
|
||||
// 标题 + 详情 一行
|
||||
titleLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stTitle.Render("MEMORY"),
|
||||
" ", stLabel.Render("已用 "), stVal.Render(usedGB),
|
||||
" / ", stVal.Render(totalGB),
|
||||
" ", pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct)),
|
||||
)
|
||||
|
||||
// 进度条
|
||||
barW := innerW - 8
|
||||
@@ -694,19 +696,10 @@ func (m model) viewMemBox(innerW int) string {
|
||||
barW = 10
|
||||
}
|
||||
bar := renderBar(pct, barW)
|
||||
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr)
|
||||
|
||||
// 详情
|
||||
usedGB := fmtBytes(m.mem.UsedBytes)
|
||||
totalGB := fmtBytes(m.mem.TotalBytes)
|
||||
detailLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stLabel.Render("已用 "), stVal.Render(usedGB),
|
||||
" ",
|
||||
stLabel.Render("总计 "), stVal.Render(totalGB),
|
||||
)
|
||||
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar)
|
||||
|
||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleLine, "", pctStr, barLine, "", detailLine,
|
||||
titleLine, barLine,
|
||||
)
|
||||
return stBox.Width(innerW + 4).Render(body)
|
||||
}
|
||||
@@ -714,7 +707,7 @@ func (m model) viewMemBox(innerW int) string {
|
||||
// ── GPU 盒子 ──
|
||||
|
||||
func (m model) viewGPUBox(innerW int) string {
|
||||
titleLine := stTitle.Render("GPU")
|
||||
titleLine := stTitle.Render("GPU") + stDim.Render(fmt.Sprintf(" (%d张)", len(m.gpus)))
|
||||
|
||||
if len(m.gpus) == 0 {
|
||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||
@@ -723,68 +716,88 @@ func (m model) viewGPUBox(innerW int) string {
|
||||
return stBox.Width(innerW + 4).Render(body)
|
||||
}
|
||||
|
||||
// 总功耗汇总
|
||||
totalP := 0.0
|
||||
totalFan := 0.0
|
||||
fanCnt := 0
|
||||
for _, g := range m.gpus {
|
||||
totalP += g.Power
|
||||
if g.FanSpeed > 0 {
|
||||
totalFan += g.FanSpeed
|
||||
fanCnt++
|
||||
}
|
||||
}
|
||||
avgFan := 0.0
|
||||
if fanCnt > 0 {
|
||||
avgFan = totalFan / float64(fanCnt)
|
||||
}
|
||||
summary := stDim.Render(" ") + stVal.Render(fmt.Sprintf("总功耗: %.0fW", totalP)) +
|
||||
stDim.Render(" │ ") + stVal.Render(fmt.Sprintf("均温: %.0f°C", m.avgTemp())) +
|
||||
stDim.Render(" │ ") + stVal.Render(fmt.Sprintf("风扇: %.0f%%", avgFan))
|
||||
|
||||
var blocks []string
|
||||
blocks = append(blocks, titleLine, "")
|
||||
blocks = append(blocks, titleLine, summary, "")
|
||||
|
||||
// 表头
|
||||
hdr := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stDim.Render(fmt.Sprintf(" %-4s", "#")),
|
||||
stDim.Render(fmt.Sprintf("%-6s", "Temp")),
|
||||
stDim.Render(fmt.Sprintf("%-6s", "Load")),
|
||||
stDim.Render(fmt.Sprintf("%-8s", "Power")),
|
||||
stDim.Render(fmt.Sprintf("%-10s", "Clock")),
|
||||
stDim.Render(fmt.Sprintf("%-10s", "MemClk")),
|
||||
stDim.Render(fmt.Sprintf("%-10s", "VRAM")),
|
||||
stDim.Render(fmt.Sprintf("%-5s", "Fan")),
|
||||
)
|
||||
blocks = append(blocks, hdr, stDim.Render(" "+strings.Repeat("─", innerW-4)))
|
||||
|
||||
for i, g := range m.gpus {
|
||||
if i > 0 {
|
||||
blocks = append(blocks, stDim.Render(" "+strings.Repeat("·", innerW-4)))
|
||||
tmpC := tempStyle(&g.Temperature)
|
||||
loadC := pctStyle(g.LoadPercent)
|
||||
|
||||
vramStr := "—"
|
||||
if g.VRAMTotalMB > 0 {
|
||||
vramStr = fmt.Sprintf("%.0f/%.0fM", g.VRAMUsedMB, g.VRAMTotalMB)
|
||||
}
|
||||
blocks = append(blocks, m.viewSingleGPU(g, innerW))
|
||||
|
||||
fanStr := "—"
|
||||
if g.FanSpeed > 0 {
|
||||
fanStr = fmt.Sprintf("%.0f%%", g.FanSpeed)
|
||||
}
|
||||
|
||||
line := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stVal.Render(fmt.Sprintf(" %-4d", i)),
|
||||
tmpC.Render(fmt.Sprintf("%-6.0f°C", g.Temperature)),
|
||||
loadC.Render(fmt.Sprintf("%-6.0f%%", g.LoadPercent)),
|
||||
stVal.Render(fmt.Sprintf("%-8.0fW", g.Power)),
|
||||
stVal.Render(fmt.Sprintf("%-10.0f", g.CoreClock)),
|
||||
stVal.Render(fmt.Sprintf("%-10.0f", g.MemClock)),
|
||||
stVal.Render(fmt.Sprintf("%-10s", vramStr)),
|
||||
stVal.Render(fmt.Sprintf("%-5s", fanStr)),
|
||||
)
|
||||
blocks = append(blocks, line)
|
||||
}
|
||||
|
||||
// 总功耗
|
||||
totalP = 0.0
|
||||
for _, g := range m.gpus {
|
||||
totalP += g.Power
|
||||
}
|
||||
blocks = append(blocks, stDim.Render(" ")+stVal.Render(fmt.Sprintf("总功耗: %.0fW", totalP)), "")
|
||||
|
||||
body := lipgloss.JoinVertical(lipgloss.Left, blocks...)
|
||||
return stBox.Width(innerW + 4).Render(body)
|
||||
}
|
||||
|
||||
func (m model) viewSingleGPU(g GPUInfo, w int) string {
|
||||
// 名称
|
||||
nm := g.Name
|
||||
if len(nm) > w-2 && w > 5 {
|
||||
nm = nm[:w-5] + "…"
|
||||
func (m model) avgTemp() float64 {
|
||||
if len(m.gpus) == 0 {
|
||||
return 0
|
||||
}
|
||||
nameLine := stBold.Render(" " + nm)
|
||||
|
||||
// 统计行
|
||||
loadC := pctStyle(g.LoadPercent)
|
||||
tmpC := tempStyle(&g.Temperature)
|
||||
statsLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stLabel.Render(" 负载 "), loadC.Render(fmt.Sprintf("%.0f%%", g.LoadPercent)),
|
||||
" ",
|
||||
stLabel.Render("温度 "), tmpC.Render(fmt.Sprintf("%.0f°C", g.Temperature)),
|
||||
" ",
|
||||
stLabel.Render("功耗 "), stVal.Render(fmt.Sprintf("%.0fW", g.Power)),
|
||||
)
|
||||
|
||||
// 频率行
|
||||
clockLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stLabel.Render(" 核心 "), stVal.Render(fmt.Sprintf("%.0f MHz", g.CoreClock)),
|
||||
" ",
|
||||
stLabel.Render("显存 "), stVal.Render(fmt.Sprintf("%.0f MHz", g.MemClock)),
|
||||
)
|
||||
|
||||
// VRAM
|
||||
vramLine := ""
|
||||
if g.VRAMTotalMB > 0 {
|
||||
vpct := g.VRAMUsedMB / g.VRAMTotalMB * 100
|
||||
barW := w - 20
|
||||
if barW < 6 {
|
||||
barW = 6
|
||||
}
|
||||
vbar := renderBar(vpct, barW)
|
||||
usedGB := fmt.Sprintf("%.1f", g.VRAMUsedMB/1024)
|
||||
totalGB := fmt.Sprintf("%.1f", g.VRAMTotalMB/1024)
|
||||
vramLine = lipgloss.JoinHorizontal(lipgloss.Left,
|
||||
stLabel.Render(" 显存 "),
|
||||
pctStyle(vpct).Render(usedGB),
|
||||
stVal.Render("/"+totalGB+" GB "),
|
||||
vbar,
|
||||
)
|
||||
total := 0.0
|
||||
for _, g := range m.gpus {
|
||||
total += g.Temperature
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left,
|
||||
nameLine, statsLine, clockLine, vramLine,
|
||||
)
|
||||
return total / float64(len(m.gpus))
|
||||
}
|
||||
|
||||
// ── History 盒子 ──
|
||||
@@ -968,6 +981,9 @@ func collectLoop(done <-chan struct{}, wsURL, name string, interval time.Duratio
|
||||
case dataCh <- data:
|
||||
default: // 满了就丢弃
|
||||
}
|
||||
|
||||
// 每 10 轮触发一次 GC,减少内存占用
|
||||
runtime.GC()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,27 +1030,72 @@ func collectCPU(perCore []float64) CPUInfo {
|
||||
}
|
||||
|
||||
func getCPUTemp() *float64 {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
||||
"(Get-CimInstance MSAcpi_ThermalZoneTemperature -Namespace 'root/wmi' -EA SilentlyContinue | Select -First 1).CurrentTemperature",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
||||
"(Get-CimInstance MSAcpi_ThermalZoneTemperature -Namespace 'root/wmi' -EA SilentlyContinue | Select -First 1).CurrentTemperature",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
val, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil || val < 1000 {
|
||||
return nil
|
||||
}
|
||||
c := (val - 2732) / 10.0
|
||||
if c < 0 || c > 150 {
|
||||
return nil
|
||||
}
|
||||
return &c
|
||||
case "linux":
|
||||
out, err := exec.CommandContext(ctx, "cat", "/sys/class/thermal/thermal_zone0/temp").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
val, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
c := val / 1000.0
|
||||
if c < 0 || c > 150 {
|
||||
return nil
|
||||
}
|
||||
return &c
|
||||
case "darwin":
|
||||
out, err := exec.CommandContext(ctx, "sudo", "powermetrics", "--samplers", "smc", "-i", "1", "-n", "1", "--format", "plist").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
s := string(out)
|
||||
idx := strings.Index(s, "<key>CPU die temperature</key>")
|
||||
if idx < 0 {
|
||||
return nil
|
||||
}
|
||||
s = s[idx:]
|
||||
idx = strings.Index(s, "<real>")
|
||||
if idx < 0 {
|
||||
return nil
|
||||
}
|
||||
s = s[idx+6:]
|
||||
end := strings.Index(s, "</real>")
|
||||
if end < 0 {
|
||||
return nil
|
||||
}
|
||||
val, err := strconv.ParseFloat(strings.TrimSpace(s[:end]), 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if val < 0 || val > 150 {
|
||||
return nil
|
||||
}
|
||||
return &val
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
val, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil || val < 1000 {
|
||||
return nil
|
||||
}
|
||||
c := (val - 2732) / 10.0
|
||||
if c < 0 || c > 150 {
|
||||
return nil
|
||||
}
|
||||
return &c
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectMemory() MemInfo {
|
||||
@@ -1050,43 +1111,119 @@ func collectMemory() MemInfo {
|
||||
}
|
||||
|
||||
func collectMotherboard() BoardInfo {
|
||||
if runtime.GOOS != "windows" {
|
||||
return BoardInfo{}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
||||
if err != nil {
|
||||
return BoardInfo{}
|
||||
}
|
||||
var b BoardInfo
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Manufacturer=") {
|
||||
b.Manufacturer = strings.TrimPrefix(line, "Manufacturer=")
|
||||
} else if strings.HasPrefix(line, "Product=") {
|
||||
b.Product = strings.TrimPrefix(line, "Product=")
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
||||
if err != nil {
|
||||
return BoardInfo{}
|
||||
}
|
||||
var b BoardInfo
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Manufacturer=") {
|
||||
b.Manufacturer = strings.TrimPrefix(line, "Manufacturer=")
|
||||
} else if strings.HasPrefix(line, "Product=") {
|
||||
b.Product = strings.TrimPrefix(line, "Product=")
|
||||
}
|
||||
}
|
||||
return b
|
||||
case "linux":
|
||||
man, _ := os.ReadFile("/sys/class/dmi/id/board_vendor")
|
||||
prod, _ := os.ReadFile("/sys/class/dmi/id/board_name")
|
||||
return BoardInfo{
|
||||
Manufacturer: strings.TrimSpace(string(man)),
|
||||
Product: strings.TrimSpace(string(prod)),
|
||||
}
|
||||
case "darwin":
|
||||
out, err := exec.CommandContext(ctx, "system_profiler", "SPHardwareDataType").Output()
|
||||
if err != nil {
|
||||
return BoardInfo{}
|
||||
}
|
||||
var b BoardInfo
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Model Name:") {
|
||||
b.Product = strings.TrimSpace(strings.TrimPrefix(line, "Model Name:"))
|
||||
} else if strings.HasPrefix(line, "Model Identifier:") {
|
||||
if b.Manufacturer == "" {
|
||||
b.Manufacturer = "Apple"
|
||||
}
|
||||
}
|
||||
}
|
||||
if b.Manufacturer == "" {
|
||||
b.Manufacturer = "Apple"
|
||||
}
|
||||
return b
|
||||
}
|
||||
return b
|
||||
return BoardInfo{}
|
||||
}
|
||||
|
||||
func collectGPUs() []GPUInfo {
|
||||
if runtime.GOOS == "windows" {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
gpus := tryNvidiaSMI()
|
||||
if len(gpus) > 0 {
|
||||
return gpus
|
||||
}
|
||||
return tryWMIGPU()
|
||||
wmi := tryWMIGPU()
|
||||
return filterPCIeGPUs(wmi)
|
||||
case "linux":
|
||||
gpus := tryNvidiaSMI()
|
||||
if len(gpus) > 0 {
|
||||
return gpus
|
||||
}
|
||||
lspci := tryLinuxGPU()
|
||||
return filterPCIeGPUs(lspci)
|
||||
case "darwin":
|
||||
gpus := tryNvidiaSMI()
|
||||
mac := tryMacGPU()
|
||||
return filterPCIeGPUs(append(gpus, mac...))
|
||||
}
|
||||
return tryNvidiaSMI()
|
||||
}
|
||||
|
||||
func filterPCIeGPUs(gpus []GPUInfo) []GPUInfo {
|
||||
var result []GPUInfo
|
||||
for _, g := range gpus {
|
||||
if !isVirtualGPU(g.Name) {
|
||||
result = append(result, g)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isVirtualGPU(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
blacklist := []string{
|
||||
"microsoft", "basic render", "vmware", "virtual", "hyper-v",
|
||||
"parallels", "virtualbox", "intel", "remote", "rdp", "citrix",
|
||||
"teamviewer", "anydesk", "parsec", "splashtop", "oray",
|
||||
"sunlogin", "todesk", "amdgpu-pro",
|
||||
}
|
||||
for _, v := range blacklist {
|
||||
if strings.Contains(name, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryNvidiaSMI() []GPUInfo {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "nvidia-smi",
|
||||
"--query-gpu=index,name,memory.total,memory.used,memory.free,temperature.gpu,utilization.gpu,clocks.current.graphics,clocks.current.memory,power.draw,fan.speed",
|
||||
"--query-gpu=index,name,uuid,pci.bus_id,driver_version,vbios_version,"+
|
||||
"temperature.gpu,temperature.memory,fan.speed,"+
|
||||
"power.draw,power.limit,power.default_limit,power.min_limit,power.max_limit,"+
|
||||
"clocks.current.graphics,clocks.current.sm,clocks.current.memory,"+
|
||||
"clocks.max.graphics,clocks.max.memory,"+
|
||||
"utilization.gpu,utilization.memory,utilization.encoder,utilization.decoder,"+
|
||||
"memory.total,memory.used,memory.free,"+
|
||||
"ecc.errors.corrected.volatile.total,ecc.errors.uncorrected.volatile.total,"+
|
||||
"compute_mode,pstate",
|
||||
"--format=csv,noheader,nounits",
|
||||
).Output()
|
||||
if err != nil {
|
||||
@@ -1095,27 +1232,64 @@ func tryNvidiaSMI() []GPUInfo {
|
||||
var gpus []GPUInfo
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) < 10 {
|
||||
if len(parts) < 24 {
|
||||
continue
|
||||
}
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
parts[i] = strings.Trim(parts[i], "[]")
|
||||
}
|
||||
|
||||
// 0:index 1:name 2:uuid 3:bus_id 4:driver 5:vbios
|
||||
// 6:temp_gpu 7:temp_mem 8:fan 9:power 10:power_limit
|
||||
// 11:power_default 12:power_min 13:power_max
|
||||
// 14:clk_gfx 15:clk_sm 16:clk_mem 17:clk_max_gfx 18:clk_max_mem
|
||||
// 19:util_gpu 20:util_mem 21:util_enc 22:util_dec
|
||||
// 23:vram_total 24:vram_used 25:vram_free
|
||||
// 26:ecc_corr 27:ecc_uncorr 28:compute_mode 29:pstate
|
||||
gpus = append(gpus, GPUInfo{
|
||||
Name: parts[1],
|
||||
VRAMTotalMB: parseFloat(parts[2]),
|
||||
VRAMUsedMB: parseFloat(parts[3]),
|
||||
Temperature: parseFloat(parts[5]),
|
||||
LoadPercent: parseFloat(parts[6]),
|
||||
CoreClock: parseFloat(parts[7]),
|
||||
MemClock: parseFloat(parts[8]),
|
||||
Power: parseFloat(parts[9]),
|
||||
FanSpeed: parseFloat(parts[10]),
|
||||
Name: cleanStr(parts[1]),
|
||||
DriverVersion: cleanStr(parts[4]),
|
||||
Temperature: parseFloat(parts[6]),
|
||||
FanSpeed: parseFloat(parts[8]),
|
||||
Power: parseFloat(parts[9]),
|
||||
PowerLimit: parseFloat(parts[10]),
|
||||
PowerMax: parseFloat(parts[13]),
|
||||
CoreClock: parseFloat(parts[14]),
|
||||
MemClock: parseFloat(parts[16]),
|
||||
ClocksMaxGfx: parseFloat(parts[17]),
|
||||
ClocksMaxMem: parseFloat(parts[18]),
|
||||
LoadPercent: parseFloat(parts[19]),
|
||||
UtilMemory: parseFloat(parts[20]),
|
||||
UtilEncoder: parseFloat(parts[21]),
|
||||
UtilDecoder: parseFloat(parts[22]),
|
||||
VRAMTotalMB: parseFloat(parts[23]),
|
||||
VRAMUsedMB: parseFloat(parts[24]),
|
||||
ComputeMode: cleanStr(parts[28]),
|
||||
PState: cleanStr(parts[29]),
|
||||
})
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
func parseFloatSafe(s string) float64 {
|
||||
s = strings.Trim(s, "[] ")
|
||||
s = strings.TrimSuffix(s, "W")
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "N/A" || s == "" {
|
||||
return 0
|
||||
}
|
||||
return parseFloat(s)
|
||||
}
|
||||
|
||||
func cleanStr(s string) string {
|
||||
s = strings.Trim(s, "[] ")
|
||||
if s == "N/A" || s == "[N/A]" {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func tryWMIGPU() []GPUInfo {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -1148,6 +1322,76 @@ func tryWMIGPU() []GPUInfo {
|
||||
return gpus
|
||||
}
|
||||
|
||||
func tryLinuxGPU() []GPUInfo {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "lspci", "-vnn").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var gpus []GPUInfo
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "VGA") || strings.Contains(line, "3D") || strings.Contains(line, "Display") {
|
||||
name := line
|
||||
if idx := strings.Index(name, ": "); idx >= 0 {
|
||||
name = name[idx+2:]
|
||||
}
|
||||
if idx := strings.Index(name, " ["); idx >= 0 {
|
||||
name = name[:idx]
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
var vram float64
|
||||
for j := i + 1; j < len(lines) && j < i+10; j++ {
|
||||
if strings.Contains(lines[j], "prefetchable") && strings.Contains(lines[j], "Memory") {
|
||||
if idx := strings.Index(lines[j], "size="); idx >= 0 {
|
||||
sz := lines[j][idx+5:]
|
||||
if end := strings.Index(sz, "M"); end > 0 {
|
||||
vram, _ = strconv.ParseFloat(sz[:end], 64)
|
||||
} else if end := strings.Index(sz, "G"); end > 0 {
|
||||
v, _ := strconv.ParseFloat(sz[:end], 64)
|
||||
vram = v * 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gpus = append(gpus, GPUInfo{Name: name, VRAMTotalMB: vram})
|
||||
}
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
func tryMacGPU() []GPUInfo {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "system_profiler", "SPDisplaysDataType", "-json").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var data struct {
|
||||
SPDisplaysDataType []struct {
|
||||
sppci_model string `json:"_name"`
|
||||
sppci_memory string `json:"sppci_memory"`
|
||||
SPDisplaysVendorID string `json:"spdisplays_vendor-id"`
|
||||
} `json:"SPDisplaysDataType"`
|
||||
}
|
||||
if json.Unmarshal(out, &data) != nil {
|
||||
return nil
|
||||
}
|
||||
var gpus []GPUInfo
|
||||
for _, d := range data.SPDisplaysDataType {
|
||||
g := GPUInfo{Name: d.sppci_model}
|
||||
if vram, err := strconv.ParseFloat(d.sppci_memory, 64); err == nil {
|
||||
g.VRAMTotalMB = vram / (1024 * 1024)
|
||||
if g.VRAMTotalMB > 1024*10 {
|
||||
g.VRAMTotalMB = vram / (1024 * 1024)
|
||||
}
|
||||
}
|
||||
gpus = append(gpus, g)
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
func parseFloat(s string) float64 {
|
||||
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
||||
return v
|
||||
|
||||
+120
-46
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -71,26 +72,22 @@ func resolvePath(baseDir, p string) string {
|
||||
// 配置结构
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
type AuthConfig struct {
|
||||
MinerID string `yaml:"miner_id"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Listen string `yaml:"listen"`
|
||||
DevicesFile string `yaml:"devices_file"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
MinerUID string `yaml:"miner_uid"`
|
||||
MinerAPIBase string `yaml:"miner_api_base"`
|
||||
FetchInterval int `yaml:"fetch_interval"`
|
||||
HistoryMax int `yaml:"history_max"`
|
||||
Listen string `yaml:"listen"`
|
||||
DevicesFile string `yaml:"devices_file"`
|
||||
MinerUID string `yaml:"miner_uid"`
|
||||
Password string `yaml:"password"`
|
||||
MinerAPIBase string `yaml:"miner_api_base"`
|
||||
FetchInterval int `yaml:"fetch_interval"`
|
||||
HistoryMax int `yaml:"history_max"`
|
||||
}
|
||||
|
||||
func defaultConfig() ServerConfig {
|
||||
return ServerConfig{
|
||||
Listen: ":8080",
|
||||
DevicesFile: "data/devices.yaml",
|
||||
Auth: AuthConfig{MinerID: "admin", Password: "123456"},
|
||||
MinerUID: "",
|
||||
Password: "123456",
|
||||
MinerAPIBase: "https://pool.kryptex.com/prl/api/v3/miner/workers",
|
||||
FetchInterval: 30,
|
||||
HistoryMax: 200,
|
||||
@@ -195,11 +192,8 @@ func fillDefaults(cfg *ServerConfig) {
|
||||
if cfg.DevicesFile == "" {
|
||||
cfg.DevicesFile = "data/devices.yaml"
|
||||
}
|
||||
if cfg.Auth.MinerID == "" {
|
||||
cfg.Auth.MinerID = "admin"
|
||||
}
|
||||
if cfg.Auth.Password == "" {
|
||||
cfg.Auth.Password = "123456"
|
||||
if cfg.Password == "" {
|
||||
cfg.Password = "123456"
|
||||
}
|
||||
if cfg.MinerAPIBase == "" {
|
||||
cfg.MinerAPIBase = "https://pool.kryptex.com/prl/api/v3/miner/workers"
|
||||
@@ -318,15 +312,16 @@ type BrowserCmd struct {
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
type Hub struct {
|
||||
mu sync.RWMutex
|
||||
fileMu sync.Mutex
|
||||
records map[string]*DeviceRecord
|
||||
live map[string]json.RawMessage
|
||||
monitors map[*websocket.Conn]struct{}
|
||||
miningData map[string]*MiningData
|
||||
hashHist map[string][]HashratePoint
|
||||
devicesAbs string
|
||||
config ServerConfig
|
||||
mu sync.RWMutex
|
||||
fileMu sync.Mutex
|
||||
records map[string]*DeviceRecord
|
||||
live map[string]json.RawMessage
|
||||
monitors map[*websocket.Conn]struct{}
|
||||
miningData map[string]*MiningData
|
||||
hashHist map[string][]HashratePoint
|
||||
devicesAbs string
|
||||
config ServerConfig
|
||||
lastWorkerCount int
|
||||
}
|
||||
|
||||
func NewHub(cfg ServerConfig, devicesAbs string) *Hub {
|
||||
@@ -380,6 +375,72 @@ func (h *Hub) saveYAML() {
|
||||
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
|
||||
}
|
||||
|
||||
func filterGPURaw(raw json.RawMessage) json.RawMessage {
|
||||
var data map[string]interface{}
|
||||
if json.Unmarshal(raw, &data) != nil {
|
||||
return raw
|
||||
}
|
||||
gpusRaw, ok := data["gpus"]
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
gpus, ok := gpusRaw.([]interface{})
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
var filtered []interface{}
|
||||
for _, g := range gpus {
|
||||
gpu, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
gpuName, _ := gpu["name"].(string)
|
||||
if isVirtualGPU(gpuName) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, g)
|
||||
}
|
||||
data["gpus"] = filtered
|
||||
out, _ := json.Marshal(data)
|
||||
return out
|
||||
}
|
||||
|
||||
func isVirtualGPU(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
blacklist := []string{
|
||||
"microsoft",
|
||||
"basic render",
|
||||
"vmware",
|
||||
"virtual",
|
||||
"hyper-v",
|
||||
"parallels",
|
||||
"virtualbox",
|
||||
"intel uhd",
|
||||
"intel iris",
|
||||
"intel(r) uhd",
|
||||
"intel(r) iris",
|
||||
"uhd graphics",
|
||||
"iris graphics",
|
||||
"remote",
|
||||
"rdp",
|
||||
"citrix",
|
||||
"teamviewer",
|
||||
"anydesk",
|
||||
"parsec",
|
||||
"splashtop",
|
||||
"amdgpu-pro",
|
||||
"oray",
|
||||
"sunlogin",
|
||||
"todesk",
|
||||
}
|
||||
for _, v := range blacklist {
|
||||
if strings.Contains(name, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
var p AgentPayload
|
||||
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
|
||||
@@ -404,7 +465,7 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
|
||||
}
|
||||
cp := *rec
|
||||
h.live[name] = raw
|
||||
h.live[name] = filterGPURaw(raw)
|
||||
var md *MiningData
|
||||
var hist []HashratePoint
|
||||
if m, ok := h.miningData[name]; ok {
|
||||
@@ -422,10 +483,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{
|
||||
Type: "device", Machine: name,
|
||||
Device: &BrowserDevice{Info: &cp, Live: raw, Online: true, Mining: md, History: hist},
|
||||
Device: &BrowserDevice{Info: &cp, Live: filterGPURaw(raw), Online: true, Mining: md, History: hist},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Hub) DeleteDevice(name string) {
|
||||
h.mu.Lock()
|
||||
_, ok := h.records[name]
|
||||
delete(h.records, name)
|
||||
delete(h.live, name)
|
||||
h.mu.Unlock()
|
||||
if ok {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
||||
logInfo("delete", "%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) MarkOffline(name string) {
|
||||
h.mu.Lock()
|
||||
_, had := h.live[name]
|
||||
@@ -455,19 +529,6 @@ func (h *Hub) MarkOffline(name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) DeleteDevice(name string) {
|
||||
h.mu.Lock()
|
||||
_, ok := h.records[name]
|
||||
delete(h.records, name)
|
||||
delete(h.live, name)
|
||||
h.mu.Unlock()
|
||||
if ok {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
||||
logInfo("delete", "%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) AddMonitor(c *websocket.Conn) {
|
||||
h.mu.Lock()
|
||||
h.monitors[c] = struct{}{}
|
||||
@@ -623,7 +684,13 @@ func (h *Hub) fetchMinerData(url string) {
|
||||
h.mu.RUnlock()
|
||||
h.broadcast(BrowserMsg{Type: "mining", Machine: w.Worker, Mining: md, History: hist})
|
||||
}
|
||||
logInfo("miner", "更新 %d 个 worker", len(apiResp.Results))
|
||||
h.mu.Lock()
|
||||
cnt := len(apiResp.Results)
|
||||
if cnt != h.lastWorkerCount {
|
||||
logInfo("miner", "worker 数量变更: %d → %d", h.lastWorkerCount, cnt)
|
||||
h.lastWorkerCount = cnt
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
@@ -639,6 +706,13 @@ var upgrader = websocket.Upgrader{
|
||||
func handleAgent(conn *websocket.Conn, hub *Hub) {
|
||||
defer conn.Close()
|
||||
addr := conn.RemoteAddr().String()
|
||||
hub.mu.RLock()
|
||||
monCnt := len(hub.monitors)
|
||||
hub.mu.RUnlock()
|
||||
if monCnt == 0 {
|
||||
logInfo("agent", "拒绝 %s (无浏览器连接)", addr)
|
||||
return
|
||||
}
|
||||
logInfo("agent+", "%s", addr)
|
||||
defer logInfo("agent-", "%s", addr)
|
||||
for {
|
||||
@@ -677,7 +751,7 @@ func handleMonitor(conn *websocket.Conn, hub *Hub) {
|
||||
}
|
||||
|
||||
// 校验
|
||||
if cmd.MinerID == hub.config.Auth.MinerID && cmd.Password == hub.config.Auth.Password {
|
||||
if cmd.MinerID == hub.config.MinerUID && cmd.Password == hub.config.Password {
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
@@ -784,9 +858,9 @@ func main() {
|
||||
if len(uid) > 22 {
|
||||
uid = uid[:22] + "..."
|
||||
}
|
||||
fmt.Printf(" ║ Miner : %-29s║\n", uid)
|
||||
fmt.Printf(" ║ Miner UID : %-29s║\n", uid)
|
||||
} else {
|
||||
fmt.Println(" ║ Miner : 未配置 ║")
|
||||
fmt.Println(" ║ Miner UID : 未配置 ║")
|
||||
}
|
||||
fmt.Println(" ╚═══════════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
|
||||
+1279
-1113
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -4,9 +4,7 @@ listen: ":8080"
|
||||
# 设备注册文件
|
||||
devices_file: "data/devices.yaml"
|
||||
# ─── 登录认证 ───
|
||||
auth:
|
||||
miner_id: "admin"
|
||||
password: "123456"
|
||||
password: "adminadmin"
|
||||
# ─── 矿池配置 ───
|
||||
# Kryptex 矿池 UID (钱包地址)
|
||||
miner_uid: "prl1prkg7des097jx9lw22kusw75s0uy0xfmxgmr7l7r67csmpxzad4lqsaajgt"
|
||||
|
||||
Reference in New Issue
Block a user