Files
MuyueWorkspace/internal/api/metrics_windows.go
Muyue d557b8e74c
All checks were successful
PR Check / check (pull_request) Successful in 1m0s
fix(windows): native ConPTY + kernel32 metrics + agent loop cap (v0.7.6)
Three issues reported on Windows + one user-requested limit bump:

1. Dashboard CPU/RAM/Network all at 0
   handleSystemMetrics read /proc/* exclusively. Replaced with a
   platform-split:
   - metrics_unix.go (!windows): existing /proc reading code.
   - metrics_windows.go: kernel32!GetSystemTimes for CPU
     (delta of idle vs kernel+user FILETIMEs) and
     kernel32!GlobalMemoryStatusEx for memory. Network left at zero
     for now — MIB_IF_ROW2 is too version-sensitive to parse by hand.
   handlers_info.go::handleSystemMetrics reduced to one delegating
   call.

2. Terminal black screen on Windows
   creack/pty/v2 returns "unsupported" on Windows; the v0.7.1 pipe
   fallback works but pipes don't carry TTY signals, so cmd/pwsh/wsl
   go silent. Implemented native ConPTY:
   - terminal_conpty_windows.go: CreatePseudoConsole + STARTUPINFOEX
     + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE wiring via
     windows.NewProcThreadAttributeList. CreateProcessW launches
     child with the PC attached, full ANSI / line discipline /
     resize.
   - canUseConPTY() probes once at startup (Win10 1809+ check).
   - Restructure: terminal_session.go now holds just the interface
     + ptySession + pipeSession structs. terminal_session_unix.go
     wires creack/pty. terminal_session_windows.go tries ConPTY
     first, falls back to pipeSession.

3. Agent stops after 15 tool calls
   MaxToolIterations bumped 15 → 500. Doc comment explains why the
   cap exists at all (infinite-loop safety) and that 500 is well
   above realistic usage.

- internal/version/version.go: 0.7.5 → 0.7.6
- CHANGELOG.md: v0.7.6 entry covers the three fixes
2026-04-27 14:04:41 +02:00

130 lines
3.3 KiB
Go

//go:build windows
package api
import (
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// collectSystemMetrics reads CPU% and memory from kernel32 directly.
// Network throughput on Windows is left at zero for now — the iphlpapi
// MIB_IF_ROW2 layout is large and version-sensitive; reliable net stats
// would warrant a separate, well-tested implementation. CPU + RAM are
// enough for the dashboard's main signal.
func collectSystemMetrics() sysMetrics {
m := sysMetrics{}
if cpu, ok := readWindowsCPUPercent(); ok {
m.CPUPercent = cpu
}
if memTotalMB, memUsedMB, memPct, ok := readWindowsMemory(); ok {
m.MemTotalMB = memTotalMB
m.MemUsedMB = memUsedMB
m.MemPercent = memPct
}
// Net: zero (TODO).
return m
}
// --- CPU ---------------------------------------------------------------
var (
cpuOnce sync.Once
getSystemTimes *syscall.LazyProc
lastWinCPUIdle uint64
lastWinCPUTotal uint64
lastWinCPUSet bool
winCPUMu sync.Mutex
)
func loadCPUFns() {
cpuOnce.Do(func() {
k := syscall.NewLazyDLL("kernel32.dll")
getSystemTimes = k.NewProc("GetSystemTimes")
})
}
func filetimeToUint64(low, high uint32) uint64 {
return uint64(high)<<32 | uint64(low)
}
// readWindowsCPUPercent samples GetSystemTimes twice and computes the busy
// ratio as 1 - dIdle / (dKernel + dUser). The first call returns 0% and
// stores the baseline; subsequent calls return the delta-based percentage.
func readWindowsCPUPercent() (float64, bool) {
loadCPUFns()
if getSystemTimes == nil {
return 0, false
}
var idle, kernel, user windows.Filetime
r1, _, _ := getSystemTimes.Call(
uintptr(unsafe.Pointer(&idle)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r1 == 0 {
return 0, false
}
idleT := filetimeToUint64(idle.LowDateTime, idle.HighDateTime)
totalT := filetimeToUint64(kernel.LowDateTime, kernel.HighDateTime) +
filetimeToUint64(user.LowDateTime, user.HighDateTime)
winCPUMu.Lock()
defer winCPUMu.Unlock()
if !lastWinCPUSet {
lastWinCPUIdle = idleT
lastWinCPUTotal = totalT
lastWinCPUSet = true
return 0, true
}
dIdle := idleT - lastWinCPUIdle
dTotal := totalT - lastWinCPUTotal
lastWinCPUIdle = idleT
lastWinCPUTotal = totalT
if dTotal == 0 {
return 0, true
}
pct := (1 - float64(dIdle)/float64(dTotal)) * 100
if pct < 0 {
pct = 0
} else if pct > 100 {
pct = 100
}
return pct, true
}
// --- Memory ------------------------------------------------------------
type memoryStatusEx struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
var globalMemoryStatusEx = syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx")
func readWindowsMemory() (totalMB, usedMB, percent float64, ok bool) {
var ms memoryStatusEx
ms.Length = uint32(unsafe.Sizeof(ms))
r1, _, _ := globalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms)))
if r1 == 0 {
return 0, 0, 0, false
}
const mb = 1024 * 1024
totalMB = float64(ms.TotalPhys) / mb
usedMB = float64(ms.TotalPhys-ms.AvailPhys) / mb
if ms.TotalPhys > 0 {
percent = float64(ms.TotalPhys-ms.AvailPhys) * 100 / float64(ms.TotalPhys)
}
return totalMB, usedMB, percent, true
}