Compare commits
18 Commits
742928a93d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f2845f6 | |||
| 2f6bea41d5 | |||
| 5ef7937d3c | |||
| b412b8f9d1 | |||
| 9bdc92252e | |||
| 62143a5a32 | |||
| 2c103af9d8 | |||
| d39713a196 | |||
| 8f1325800c | |||
| 2377530fab | |||
| 6605adaed5 | |||
| 7ff0973a5b | |||
| 6ebad2a065 | |||
| 7217974395 | |||
| 65310c07e9 | |||
| 33d765b124 | |||
| e06b0c1fcd | |||
| 7204f637ac |
+1
-1
@@ -8,6 +8,6 @@
|
|||||||
# 删除此文件可重新配置
|
# 删除此文件可重新配置
|
||||||
|
|
||||||
machine_name: my-pc
|
machine_name: my-pc
|
||||||
server: 192.168.1.100:8080
|
server: 127.0.0.1:8080
|
||||||
interval: 3
|
interval: 3
|
||||||
reconnect_delay: 3
|
reconnect_delay: 3
|
||||||
|
|||||||
+419
-173
@@ -134,14 +134,25 @@ type MemInfo struct {
|
|||||||
Percent float64 `json:"percent"`
|
Percent float64 `json:"percent"`
|
||||||
}
|
}
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
VRAMTotalMB float64 `json:"vram_total_mb"`
|
VRAMTotalMB float64 `json:"vram_total_mb"`
|
||||||
VRAMUsedMB float64 `json:"vram_used_mb"`
|
VRAMUsedMB float64 `json:"vram_used_mb"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
LoadPercent float64 `json:"load_percent"`
|
LoadPercent float64 `json:"load_percent"`
|
||||||
CoreClock float64 `json:"core_clock_mhz"`
|
CoreClock float64 `json:"core_clock_mhz"`
|
||||||
MemClock float64 `json:"mem_clock_mhz"`
|
MemClock float64 `json:"mem_clock_mhz"`
|
||||||
Power float64 `json:"power_watts"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
@@ -542,6 +553,8 @@ func (m model) viewMonitor() string {
|
|||||||
if tw < 60 {
|
if tw < 60 {
|
||||||
tw = 60
|
tw = 60
|
||||||
}
|
}
|
||||||
|
gap := 2
|
||||||
|
halfW := (tw - gap) / 2
|
||||||
|
|
||||||
// ── 顶栏 ──
|
// ── 顶栏 ──
|
||||||
wsDot := "○"
|
wsDot := "○"
|
||||||
@@ -561,47 +574,33 @@ func (m model) viewMonitor() string {
|
|||||||
)
|
)
|
||||||
sep := stDim.Render(strings.Repeat("─", tw))
|
sep := stDim.Render(strings.Repeat("─", tw))
|
||||||
|
|
||||||
// ── 列宽计算 ──
|
// ── 第一行: CPU + Memory ──
|
||||||
gap := 2
|
cpuBox := m.viewCPUBox(halfW - 4)
|
||||||
var leftW, rightW int
|
memBox := m.viewMemBox(halfW - 4)
|
||||||
if tw >= 100 {
|
row1 := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
leftW = (tw - gap) / 2
|
cpuBox, strings.Repeat(" ", gap), memBox,
|
||||||
rightW = tw - gap - leftW
|
)
|
||||||
} else {
|
|
||||||
leftW = tw
|
|
||||||
rightW = tw
|
|
||||||
gap = 0
|
|
||||||
}
|
|
||||||
innerL := leftW - 4
|
|
||||||
innerR := rightW - 4
|
|
||||||
|
|
||||||
// ── 左列: CPU + History ──
|
// ── 第二行: GPU + History ──
|
||||||
cpuBox := m.viewCPUBox(innerL)
|
gpuBox := m.viewGPUBox(halfW - 4)
|
||||||
histBox := m.viewHistBox(innerL)
|
histBox := m.viewHistBox(halfW - 4)
|
||||||
leftCol := lipgloss.JoinVertical(lipgloss.Left, cpuBox, "", histBox)
|
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
|
gpuBox, strings.Repeat(" ", gap), 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)
|
|
||||||
|
|
||||||
full := lipgloss.JoinVertical(lipgloss.Left,
|
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 盒子 ──
|
// ── CPU 盒子 ──
|
||||||
@@ -610,40 +609,38 @@ func (m model) viewCPUBox(innerW int) string {
|
|||||||
c := m.cpu
|
c := m.cpu
|
||||||
pct := c.UsageTotal
|
pct := c.UsageTotal
|
||||||
|
|
||||||
// 标题
|
|
||||||
modelName := c.Model
|
modelName := c.Model
|
||||||
maxNameW := innerW - 10
|
maxNameW := innerW - 8
|
||||||
if len(modelName) > maxNameW && maxNameW > 3 {
|
if len(modelName) > maxNameW && maxNameW > 3 {
|
||||||
modelName = modelName[:maxNameW-1] + "…"
|
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
|
barW := innerW - 8
|
||||||
if barW < 10 {
|
if barW < 10 {
|
||||||
barW = 10
|
barW = 10
|
||||||
}
|
}
|
||||||
bar := renderBar(pct, barW)
|
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)
|
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)
|
coreLine := m.viewCoreHeatmap(innerW)
|
||||||
|
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
titleLine, "", barLine, infoLine, "", coreLine,
|
titleLine, barLine, coreLine,
|
||||||
)
|
)
|
||||||
return stBox.Width(innerW + 4).Render(body)
|
return stBox.Width(innerW + 4).Render(body)
|
||||||
}
|
}
|
||||||
@@ -682,10 +679,16 @@ func (m model) viewCoreHeatmap(innerW int) string {
|
|||||||
func (m model) viewMemBox(innerW int) string {
|
func (m model) viewMemBox(innerW int) string {
|
||||||
pct := m.mem.Percent
|
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
|
barW := innerW - 8
|
||||||
@@ -693,19 +696,10 @@ func (m model) viewMemBox(innerW int) string {
|
|||||||
barW = 10
|
barW = 10
|
||||||
}
|
}
|
||||||
bar := renderBar(pct, barW)
|
bar := renderBar(pct, barW)
|
||||||
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr)
|
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar)
|
||||||
|
|
||||||
// 详情
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
titleLine, "", pctStr, barLine, "", detailLine,
|
titleLine, barLine,
|
||||||
)
|
)
|
||||||
return stBox.Width(innerW + 4).Render(body)
|
return stBox.Width(innerW + 4).Render(body)
|
||||||
}
|
}
|
||||||
@@ -713,7 +707,7 @@ func (m model) viewMemBox(innerW int) string {
|
|||||||
// ── GPU 盒子 ──
|
// ── GPU 盒子 ──
|
||||||
|
|
||||||
func (m model) viewGPUBox(innerW int) string {
|
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 {
|
if len(m.gpus) == 0 {
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
@@ -722,68 +716,88 @@ func (m model) viewGPUBox(innerW int) string {
|
|||||||
return stBox.Width(innerW + 4).Render(body)
|
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
|
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 {
|
for i, g := range m.gpus {
|
||||||
if i > 0 {
|
tmpC := tempStyle(&g.Temperature)
|
||||||
blocks = append(blocks, stDim.Render(" "+strings.Repeat("·", innerW-4)))
|
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...)
|
body := lipgloss.JoinVertical(lipgloss.Left, blocks...)
|
||||||
return stBox.Width(innerW + 4).Render(body)
|
return stBox.Width(innerW + 4).Render(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) viewSingleGPU(g GPUInfo, w int) string {
|
func (m model) avgTemp() float64 {
|
||||||
// 名称
|
if len(m.gpus) == 0 {
|
||||||
nm := g.Name
|
return 0
|
||||||
if len(nm) > w-2 && w > 5 {
|
|
||||||
nm = nm[:w-5] + "…"
|
|
||||||
}
|
}
|
||||||
nameLine := stBold.Render(" " + nm)
|
total := 0.0
|
||||||
|
for _, g := range m.gpus {
|
||||||
// 统计行
|
total += g.Temperature
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return total / float64(len(m.gpus))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left,
|
|
||||||
nameLine, statsLine, clockLine, vramLine,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── History 盒子 ──
|
// ── History 盒子 ──
|
||||||
@@ -967,6 +981,9 @@ func collectLoop(done <-chan struct{}, wsURL, name string, interval time.Duratio
|
|||||||
case dataCh <- data:
|
case dataCh <- data:
|
||||||
default: // 满了就丢弃
|
default: // 满了就丢弃
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每 10 轮触发一次 GC,减少内存占用
|
||||||
|
runtime.GC()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,27 +1030,72 @@ func collectCPU(perCore []float64) CPUInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCPUTemp() *float64 {
|
func getCPUTemp() *float64 {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
|
||||||
"(Get-CimInstance MSAcpi_ThermalZoneTemperature -Namespace 'root/wmi' -EA SilentlyContinue | Select -First 1).CurrentTemperature",
|
switch runtime.GOOS {
|
||||||
).Output()
|
case "windows":
|
||||||
if err != nil {
|
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
||||||
return nil
|
"(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))
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectMemory() MemInfo {
|
func collectMemory() MemInfo {
|
||||||
@@ -1049,43 +1111,119 @@ func collectMemory() MemInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func collectMotherboard() BoardInfo {
|
func collectMotherboard() BoardInfo {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return BoardInfo{}
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
|
||||||
if err != nil {
|
switch runtime.GOOS {
|
||||||
return BoardInfo{}
|
case "windows":
|
||||||
}
|
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
||||||
var b BoardInfo
|
if err != nil {
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
return BoardInfo{}
|
||||||
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=")
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func collectGPUs() []GPUInfo {
|
||||||
if runtime.GOOS == "windows" {
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
gpus := tryNvidiaSMI()
|
gpus := tryNvidiaSMI()
|
||||||
if len(gpus) > 0 {
|
if len(gpus) > 0 {
|
||||||
return gpus
|
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()
|
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 {
|
func tryNvidiaSMI() []GPUInfo {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "nvidia-smi",
|
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",
|
"--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",
|
"--format=csv,noheader,nounits",
|
||||||
).Output()
|
).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1094,26 +1232,64 @@ func tryNvidiaSMI() []GPUInfo {
|
|||||||
var gpus []GPUInfo
|
var gpus []GPUInfo
|
||||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||||
parts := strings.Split(line, ",")
|
parts := strings.Split(line, ",")
|
||||||
if len(parts) < 10 {
|
if len(parts) < 24 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for i := range parts {
|
for i := range parts {
|
||||||
parts[i] = strings.TrimSpace(parts[i])
|
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{
|
gpus = append(gpus, GPUInfo{
|
||||||
Name: parts[1],
|
Name: cleanStr(parts[1]),
|
||||||
VRAMTotalMB: parseFloat(parts[2]),
|
DriverVersion: cleanStr(parts[4]),
|
||||||
VRAMUsedMB: parseFloat(parts[3]),
|
Temperature: parseFloat(parts[6]),
|
||||||
Temperature: parseFloat(parts[5]),
|
FanSpeed: parseFloat(parts[8]),
|
||||||
LoadPercent: parseFloat(parts[6]),
|
Power: parseFloat(parts[9]),
|
||||||
CoreClock: parseFloat(parts[7]),
|
PowerLimit: parseFloat(parts[10]),
|
||||||
MemClock: parseFloat(parts[8]),
|
PowerMax: parseFloat(parts[13]),
|
||||||
Power: parseFloat(parts[9]),
|
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
|
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 {
|
func tryWMIGPU() []GPUInfo {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1146,6 +1322,76 @@ func tryWMIGPU() []GPUInfo {
|
|||||||
return gpus
|
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 {
|
func parseFloat(s string) float64 {
|
||||||
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
||||||
return v
|
return v
|
||||||
|
|||||||
+120
-46
@@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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 {
|
type ServerConfig struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
DevicesFile string `yaml:"devices_file"`
|
DevicesFile string `yaml:"devices_file"`
|
||||||
Auth AuthConfig `yaml:"auth"`
|
MinerUID string `yaml:"miner_uid"`
|
||||||
MinerUID string `yaml:"miner_uid"`
|
Password string `yaml:"password"`
|
||||||
MinerAPIBase string `yaml:"miner_api_base"`
|
MinerAPIBase string `yaml:"miner_api_base"`
|
||||||
FetchInterval int `yaml:"fetch_interval"`
|
FetchInterval int `yaml:"fetch_interval"`
|
||||||
HistoryMax int `yaml:"history_max"`
|
HistoryMax int `yaml:"history_max"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultConfig() ServerConfig {
|
func defaultConfig() ServerConfig {
|
||||||
return ServerConfig{
|
return ServerConfig{
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
DevicesFile: "data/devices.yaml",
|
DevicesFile: "data/devices.yaml",
|
||||||
Auth: AuthConfig{MinerID: "admin", Password: "123456"},
|
MinerUID: "",
|
||||||
|
Password: "123456",
|
||||||
MinerAPIBase: "https://pool.kryptex.com/prl/api/v3/miner/workers",
|
MinerAPIBase: "https://pool.kryptex.com/prl/api/v3/miner/workers",
|
||||||
FetchInterval: 30,
|
FetchInterval: 30,
|
||||||
HistoryMax: 200,
|
HistoryMax: 200,
|
||||||
@@ -195,11 +192,8 @@ func fillDefaults(cfg *ServerConfig) {
|
|||||||
if cfg.DevicesFile == "" {
|
if cfg.DevicesFile == "" {
|
||||||
cfg.DevicesFile = "data/devices.yaml"
|
cfg.DevicesFile = "data/devices.yaml"
|
||||||
}
|
}
|
||||||
if cfg.Auth.MinerID == "" {
|
if cfg.Password == "" {
|
||||||
cfg.Auth.MinerID = "admin"
|
cfg.Password = "123456"
|
||||||
}
|
|
||||||
if cfg.Auth.Password == "" {
|
|
||||||
cfg.Auth.Password = "123456"
|
|
||||||
}
|
}
|
||||||
if cfg.MinerAPIBase == "" {
|
if cfg.MinerAPIBase == "" {
|
||||||
cfg.MinerAPIBase = "https://pool.kryptex.com/prl/api/v3/miner/workers"
|
cfg.MinerAPIBase = "https://pool.kryptex.com/prl/api/v3/miner/workers"
|
||||||
@@ -318,15 +312,16 @@ type BrowserCmd struct {
|
|||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
type Hub struct {
|
type Hub struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
fileMu sync.Mutex
|
fileMu sync.Mutex
|
||||||
records map[string]*DeviceRecord
|
records map[string]*DeviceRecord
|
||||||
live map[string]json.RawMessage
|
live map[string]json.RawMessage
|
||||||
monitors map[*websocket.Conn]struct{}
|
monitors map[*websocket.Conn]struct{}
|
||||||
miningData map[string]*MiningData
|
miningData map[string]*MiningData
|
||||||
hashHist map[string][]HashratePoint
|
hashHist map[string][]HashratePoint
|
||||||
devicesAbs string
|
devicesAbs string
|
||||||
config ServerConfig
|
config ServerConfig
|
||||||
|
lastWorkerCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHub(cfg ServerConfig, devicesAbs string) *Hub {
|
func NewHub(cfg ServerConfig, devicesAbs string) *Hub {
|
||||||
@@ -380,6 +375,72 @@ func (h *Hub) saveYAML() {
|
|||||||
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
|
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) {
|
func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||||
var p AgentPayload
|
var p AgentPayload
|
||||||
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
|
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})
|
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
|
||||||
}
|
}
|
||||||
cp := *rec
|
cp := *rec
|
||||||
h.live[name] = raw
|
h.live[name] = filterGPURaw(raw)
|
||||||
var md *MiningData
|
var md *MiningData
|
||||||
var hist []HashratePoint
|
var hist []HashratePoint
|
||||||
if m, ok := h.miningData[name]; ok {
|
if m, ok := h.miningData[name]; ok {
|
||||||
@@ -422,10 +483,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
|||||||
h.saveYAML()
|
h.saveYAML()
|
||||||
h.broadcast(BrowserMsg{
|
h.broadcast(BrowserMsg{
|
||||||
Type: "device", Machine: name,
|
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) {
|
func (h *Hub) MarkOffline(name string) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
_, had := h.live[name]
|
_, 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) {
|
func (h *Hub) AddMonitor(c *websocket.Conn) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.monitors[c] = struct{}{}
|
h.monitors[c] = struct{}{}
|
||||||
@@ -623,7 +684,13 @@ func (h *Hub) fetchMinerData(url string) {
|
|||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
h.broadcast(BrowserMsg{Type: "mining", Machine: w.Worker, Mining: md, History: hist})
|
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) {
|
func handleAgent(conn *websocket.Conn, hub *Hub) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
addr := conn.RemoteAddr().String()
|
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)
|
logInfo("agent+", "%s", addr)
|
||||||
defer logInfo("agent-", "%s", addr)
|
defer logInfo("agent-", "%s", addr)
|
||||||
for {
|
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
|
authOK = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -784,9 +858,9 @@ func main() {
|
|||||||
if len(uid) > 22 {
|
if len(uid) > 22 {
|
||||||
uid = uid[:22] + "..."
|
uid = uid[:22] + "..."
|
||||||
}
|
}
|
||||||
fmt.Printf(" ║ Miner : %-29s║\n", uid)
|
fmt.Printf(" ║ Miner UID : %-29s║\n", uid)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ║ Miner : 未配置 ║")
|
fmt.Println(" ║ Miner UID : 未配置 ║")
|
||||||
}
|
}
|
||||||
fmt.Println(" ╚═══════════════════════════════════════════╝")
|
fmt.Println(" ╚═══════════════════════════════════════════╝")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
+1277
-1099
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -4,9 +4,7 @@ listen: ":8080"
|
|||||||
# 设备注册文件
|
# 设备注册文件
|
||||||
devices_file: "data/devices.yaml"
|
devices_file: "data/devices.yaml"
|
||||||
# ─── 登录认证 ───
|
# ─── 登录认证 ───
|
||||||
auth:
|
password: "adminadmin"
|
||||||
miner_id: "admin"
|
|
||||||
password: "123456"
|
|
||||||
# ─── 矿池配置 ───
|
# ─── 矿池配置 ───
|
||||||
# Kryptex 矿池 UID (钱包地址)
|
# Kryptex 矿池 UID (钱包地址)
|
||||||
miner_uid: "prl1prkg7des097jx9lw22kusw75s0uy0xfmxgmr7l7r67csmpxzad4lqsaajgt"
|
miner_uid: "prl1prkg7des097jx9lw22kusw75s0uy0xfmxgmr7l7r67csmpxzad4lqsaajgt"
|
||||||
|
|||||||
Reference in New Issue
Block a user