36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
//go:build windows
|
|
|
|
package proc
|
|
|
|
import (
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/pranshuparmar/witr/pkg/model"
|
|
)
|
|
|
|
var procGlobalMemoryStatusEx = modkernel32.NewProc("GlobalMemoryStatusEx")
|
|
|
|
// memoryStatusEx mirrors MEMORYSTATUSEX. dwLength must be set to sizeof(struct)
|
|
// before GlobalMemoryStatusEx is called.
|
|
type memoryStatusEx struct {
|
|
Length uint32
|
|
MemoryLoad uint32
|
|
TotalPhys uint64
|
|
AvailPhys uint64
|
|
TotalPageFile uint64
|
|
AvailPageFile uint64
|
|
TotalVirtual uint64
|
|
AvailVirtual uint64
|
|
AvailExtendedVirtual uint64
|
|
}
|
|
|
|
var (
|
|
totalPhysOnce sync.Once
|
|
totalPhysVal uint64
|
|
)
|
|
|
|
// windowsTotalPhysicalMemory returns total physical RAM in bytes. The value is
|
|
// fixed for the life of the process, so it's read once and cached.
|
|
func windowsTotalPhysicalMemory() uint64 {
|
|
totalPhysOnce.Do(func() {
|
|
var ms memoryStatusEx
|
|
ms.Length = uint32(unsafe.Sizeof(ms))
|
|
if ret, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms))); ret != 0 {
|
|
totalPhysVal = ms.TotalPhys
|
|
}
|
|
})
|
|
return totalPhysVal
|
|
}
|
|
|
|
// windowsMemoryPercent expresses a resident byte count as a percentage of total
|
|
// physical RAM.
|
|
func windowsMemoryPercent(rss uint64) float64 {
|
|
total := windowsTotalPhysicalMemory()
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
return float64(rss) / float64(total) * 100.0
|
|
}
|
|
|
|
// windowsProcMetrics opens a process once and returns its resident set size
|
|
// (WorkingSetSize, in bytes), lifetime-average CPU%, total CPU time, and start
|
|
// time. All are zero values when the handle can't be opened, which is expected
|
|
// for protected/system processes without elevation — callers keep the identity
|
|
// fields and report zeros rather than failing. It reads only fixed-size kernel
|
|
// structures (no remote process-memory access), so it's safe to call across the
|
|
// full process list.
|
|
func windowsProcMetrics(pid int) (rss uint64, cpu float64, cpuTime time.Duration, started time.Time) {
|
|
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
|
if err != nil {
|
|
return 0, 0, 0, time.Time{}
|
|
}
|
|
defer syscall.CloseHandle(handle)
|
|
|
|
var pmc processMemoryCountersEx
|
|
pmc.CB = uint32(unsafe.Sizeof(pmc))
|
|
if ret, _, _ := procGetProcessMemoryInfo.Call(
|
|
uintptr(handle),
|
|
uintptr(unsafe.Pointer(&pmc)),
|
|
uintptr(pmc.CB),
|
|
); ret != 0 {
|
|
rss = uint64(pmc.WorkingSetSize)
|
|
}
|
|
|
|
var creation, exit, kernel, user syscall.Filetime
|
|
if ret, _, _ := procGetProcessTimes.Call(
|
|
uintptr(handle),
|
|
uintptr(unsafe.Pointer(&creation)),
|
|
uintptr(unsafe.Pointer(&exit)),
|
|
uintptr(unsafe.Pointer(&kernel)),
|
|
uintptr(unsafe.Pointer(&user)),
|
|
); ret != 0 {
|
|
started = time.Unix(0, creation.Nanoseconds())
|
|
cpuTime = filetimeTicksToDuration(kernel) + filetimeTicksToDuration(user)
|
|
wall := time.Since(started)
|
|
if wall > 0 {
|
|
cpu = float64(cpuTime) / float64(wall) * 100.0
|
|
}
|
|
}
|
|
|
|
return rss, cpu, cpuTime, started
|
|
}
|
|
|
|
// windowsHealth derives a health status from a process's resident memory and
|
|
// total CPU time. Windows has no zombie/stopped equivalent, so it reports the
|
|
// resource conditions, matching the Unix >2h CPU and >1GiB RSS thresholds.
|
|
func windowsHealth(rss uint64, cpuTime time.Duration) string {
|
|
switch {
|
|
case cpuTime > 2*time.Hour:
|
|
return "high-cpu"
|
|
case rss > 1<<30: // 1 GiB
|
|
return "high-mem"
|
|
default:
|
|
return "healthy"
|
|
}
|
|
}
|
|
|
|
// GetResourceContext returns CPU and memory usage for a process.
|
|
//
|
|
// CPU usage is the lifetime average — total kernel + user CPU time divided
|
|
// by wall-clock time since the process started — not an instantaneous %.
|
|
// Memory is the private commit (PrivateUsage).
|
|
func GetResourceContext(pid int) *model.ResourceContext {
|
|
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer syscall.CloseHandle(handle)
|
|
|
|
var (
|
|
cpu float64
|
|
mem uint64
|
|
)
|
|
|
|
var pmc processMemoryCountersEx
|
|
pmc.CB = uint32(unsafe.Sizeof(pmc))
|
|
if ret, _, _ := procGetProcessMemoryInfo.Call(
|
|
uintptr(handle),
|
|
uintptr(unsafe.Pointer(&pmc)),
|
|
uintptr(pmc.CB),
|
|
); ret != 0 {
|
|
mem = uint64(pmc.PrivateUsage)
|
|
}
|
|
|
|
var creation, exit, kernel, user syscall.Filetime
|
|
if ret, _, _ := procGetProcessTimes.Call(
|
|
uintptr(handle),
|
|
uintptr(unsafe.Pointer(&creation)),
|
|
uintptr(unsafe.Pointer(&exit)),
|
|
uintptr(unsafe.Pointer(&kernel)),
|
|
uintptr(unsafe.Pointer(&user)),
|
|
); ret != 0 {
|
|
startTime := time.Unix(0, creation.Nanoseconds())
|
|
wall := time.Since(startTime)
|
|
cpuTime := filetimeTicksToDuration(kernel) + filetimeTicksToDuration(user)
|
|
if wall > 0 {
|
|
cpu = float64(cpuTime) / float64(wall) * 100.0
|
|
}
|
|
}
|
|
|
|
return &model.ResourceContext{
|
|
CPUUsage: cpu,
|
|
MemoryUsage: mem,
|
|
}
|
|
}
|
|
|
|
// filetimeTicksToDuration treats a Filetime as a count of 100-ns ticks (the
|
|
// shape kernel/user time take in GetProcessTimes), not as an absolute
|
|
// timestamp.
|
|
func filetimeTicksToDuration(ft syscall.Filetime) time.Duration {
|
|
ticks := uint64(ft.HighDateTime)<<32 | uint64(ft.LowDateTime)
|
|
return time.Duration(ticks) * 100 * time.Nanosecond
|
|
}
|