feat(nodes): live CPU/RAM/disk monitoring in the node list
Build backend images / build content-svc (push) Failing after 45s
Build backend images / build file-svc (push) Failing after 55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 54s
Build backend images / build notification-svc (push) Failing after 53s
Build backend images / build render-svc (push) Failing after 47s
Build backend images / build studio-svc (push) Failing after 51s
Build backend images / build content-svc (push) Failing after 45s
Build backend images / build file-svc (push) Failing after 55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 54s
Build backend images / build notification-svc (push) Failing after 53s
Build backend images / build render-svc (push) Failing after 47s
Build backend images / build studio-svc (push) Failing after 51s
- node-agent: internal/metrics — read CPU% (GetSystemTimes), RAM (GlobalMemoryStatusEx), disk used%/total (GetDiskFreeSpaceEx) via stdlib kernel32 (no external dep; windows build + non-windows stub). Heartbeat now reports cpu_pct/ram_available_mb/disk_used_pct/ disk_total_gb + ae_running. - render-svc: heartbeat persists last_disk_pct + disk_total_gb (migration 29); RenderNode model + node SELECT/scan carry them. - admin: rewrite NodesTable to the real RenderNode shape (fixes a pre-existing items/V2Node mismatch that left the list empty) + a CPU/RAM/disk bars column + stale-heartbeat flag. - assets-bundle ingestion: ProjectMediaBundle (jszip) auto-maps project.zip → project/scene image/demo/colour + music; PatchProject gains image/full_demo/shared_colors_svg. - scan: RGBA (4-number) colours recognised + frshare single-int controls detected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
//go:build windows
|
||||
|
||||
// Package metrics reads host CPU / RAM / disk usage on Windows using only the
|
||||
// stdlib (kernel32 via syscall) — no external dependency (GOPROXY is unavailable
|
||||
// in this environment, and gopsutil isn't needed for these three numbers).
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
||||
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
|
||||
procGetDiskFreeSpaceExW = kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||
)
|
||||
|
||||
type memoryStatusEx struct {
|
||||
cbSize uint32
|
||||
dwMemoryLoad uint32
|
||||
ullTotalPhys uint64
|
||||
ullAvailPhys uint64
|
||||
ullTotalPageFile uint64
|
||||
ullAvailPageFile uint64
|
||||
ullTotalVirtual uint64
|
||||
ullAvailVirtual uint64
|
||||
ullAvailExtendedVirtual uint64
|
||||
}
|
||||
|
||||
type filetime struct{ low, high uint32 }
|
||||
|
||||
func (f filetime) u64() uint64 { return uint64(f.high)<<32 | uint64(f.low) }
|
||||
|
||||
type cpuSample struct{ idle, kernel, user uint64 }
|
||||
|
||||
func readCPU() (cpuSample, bool) {
|
||||
var idle, kernel, user filetime
|
||||
r, _, _ := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idle)),
|
||||
uintptr(unsafe.Pointer(&kernel)),
|
||||
uintptr(unsafe.Pointer(&user)),
|
||||
)
|
||||
if r == 0 {
|
||||
return cpuSample{}, false
|
||||
}
|
||||
return cpuSample{idle.u64(), kernel.u64(), user.u64()}, true
|
||||
}
|
||||
|
||||
// CPUPercent samples system CPU times over `interval` and returns busy % (0-100).
|
||||
// On Windows the "kernel" time INCLUDES idle, so total = kernel+user, busy = total-idle.
|
||||
func CPUPercent(interval time.Duration) int {
|
||||
a, ok := readCPU()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
time.Sleep(interval)
|
||||
b, ok := readCPU()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
idleD := float64(b.idle - a.idle)
|
||||
totalD := float64((b.kernel - a.kernel) + (b.user - a.user))
|
||||
if totalD <= 0 {
|
||||
return 0
|
||||
}
|
||||
busy := (totalD - idleD) / totalD * 100
|
||||
if busy < 0 {
|
||||
busy = 0
|
||||
}
|
||||
if busy > 100 {
|
||||
busy = 100
|
||||
}
|
||||
return int(busy + 0.5)
|
||||
}
|
||||
|
||||
// Memory returns (totalMB, availableMB).
|
||||
func Memory() (totalMB, availMB int) {
|
||||
var m memoryStatusEx
|
||||
m.cbSize = uint32(unsafe.Sizeof(m))
|
||||
if r, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&m))); r == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return int(m.ullTotalPhys / 1024 / 1024), int(m.ullAvailPhys / 1024 / 1024)
|
||||
}
|
||||
|
||||
// Disk returns (usedPct, totalGB) for the volume containing path (default C:\).
|
||||
func Disk(path string) (usedPct, totalGB int) {
|
||||
if path == "" {
|
||||
path = "C:\\"
|
||||
}
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
var freeAvail, total, totalFree uint64
|
||||
r, _, _ := procGetDiskFreeSpaceExW.Call(
|
||||
uintptr(unsafe.Pointer(p)),
|
||||
uintptr(unsafe.Pointer(&freeAvail)),
|
||||
uintptr(unsafe.Pointer(&total)),
|
||||
uintptr(unsafe.Pointer(&totalFree)),
|
||||
)
|
||||
if r == 0 || total == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
used := total - totalFree
|
||||
return int(float64(used)/float64(total)*100 + 0.5), int(total / 1024 / 1024 / 1024)
|
||||
}
|
||||
|
||||
// Cores returns the logical CPU count.
|
||||
func Cores() int { return runtime.NumCPU() }
|
||||
Reference in New Issue
Block a user