All checks were successful
PR Check / check (pull_request) Successful in 1m0s
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
130 lines
3.3 KiB
Go
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
|
|
}
|