feat: Windows agent inventory
This commit is contained in:
+1
-1
@@ -3,13 +3,13 @@ module gitea.hostxtra.co.uk/mrhid6/vantage/agent
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.20.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
|
||||
@@ -144,11 +144,14 @@ func partitions() []pb.PartitionReport {
|
||||
if syscall.Statfs(fields[1], &st) != nil {
|
||||
continue
|
||||
}
|
||||
total := st.Blocks * uint64(st.Bsize)
|
||||
free := st.Bavail * uint64(st.Bsize)
|
||||
bsize := uint64(st.Bsize)
|
||||
total := st.Blocks * bsize
|
||||
// Bfree, not Bavail: the difference is the root-reserved 5% on ext4,
|
||||
// which is not used space. df counts it the same way.
|
||||
used := (st.Blocks - st.Bfree) * bsize
|
||||
out = append(out, pb.PartitionReport{
|
||||
Device: fields[0], Mountpoint: fields[1], Fstype: fields[2],
|
||||
TotalBytes: total, UsedBytes: total - free,
|
||||
TotalBytes: total, UsedBytes: used,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//go:build !linux
|
||||
//go:build !linux && !windows
|
||||
|
||||
// Inventory collection is Linux-only. This no-op stands in everywhere else.
|
||||
// Inventory collection has Linux and Windows implementations. This no-op stands
|
||||
// in everywhere else.
|
||||
//
|
||||
// The build constraint above is load-bearing: "_other" is not a GOOS suffix, so
|
||||
// without it this file compiles on Linux too and collides with collect_linux.go.
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
|
||||
// x/sys/windows exposes neither of these two, so they are bound by hand.
|
||||
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
||||
)
|
||||
|
||||
func collect(r *pb.InventoryReport, includeStatic bool) {
|
||||
r.CPU.UsagePct = cpuUsage()
|
||||
// Windows has no load average. Left at zero; the UI already treats it as
|
||||
// optional because it is omitempty on the wire.
|
||||
|
||||
m := memoryStatus()
|
||||
if m.TotalPhys > m.AvailPhys {
|
||||
r.Memory.UsedBytes = m.TotalPhys - m.AvailPhys
|
||||
}
|
||||
// TotalPageFile is the commit limit — physical memory plus the pagefile —
|
||||
// so the pagefile alone is the difference.
|
||||
swapTotal := sub(m.TotalPageFile, m.TotalPhys)
|
||||
swapUsed := sub(sub(m.TotalPageFile, m.AvailPageFile), sub(m.TotalPhys, m.AvailPhys))
|
||||
if swapUsed > swapTotal {
|
||||
swapUsed = swapTotal
|
||||
}
|
||||
r.SwapUsed = swapUsed
|
||||
|
||||
if includeStatic {
|
||||
r.Memory.TotalBytes = m.TotalPhys
|
||||
r.SwapTotal = swapTotal
|
||||
r.CPU.Model, r.CPU.Cores = cpuStatic()
|
||||
r.Kernel = kernel()
|
||||
r.Partitions = partitions()
|
||||
}
|
||||
}
|
||||
|
||||
func sub(a, b uint64) uint64 {
|
||||
if a > b {
|
||||
return a - b
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type memoryStatusEx struct {
|
||||
Length uint32
|
||||
MemoryLoad uint32
|
||||
TotalPhys uint64
|
||||
AvailPhys uint64
|
||||
TotalPageFile uint64
|
||||
AvailPageFile uint64
|
||||
TotalVirtual uint64
|
||||
AvailVirtual uint64
|
||||
AvailExtendedVirtual uint64
|
||||
}
|
||||
|
||||
func memoryStatus() memoryStatusEx {
|
||||
var m memoryStatusEx
|
||||
m.Length = uint32(unsafe.Sizeof(m))
|
||||
r, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&m)))
|
||||
if r == 0 {
|
||||
return memoryStatusEx{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func systemTimes() (idle, total uint64, ok bool) {
|
||||
var idleFT, kernelFT, userFT windows.Filetime
|
||||
r, _, _ := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idleFT)),
|
||||
uintptr(unsafe.Pointer(&kernelFT)),
|
||||
uintptr(unsafe.Pointer(&userFT)),
|
||||
)
|
||||
if r == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
ft := func(f windows.Filetime) uint64 {
|
||||
return uint64(f.HighDateTime)<<32 | uint64(f.LowDateTime)
|
||||
}
|
||||
// Kernel time already includes idle time, so kernel+user is the whole.
|
||||
return ft(idleFT), ft(kernelFT) + ft(userFT), true
|
||||
}
|
||||
|
||||
func cpuUsage() float64 {
|
||||
i1, t1, ok := systemTimes()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
i2, t2, ok := systemTimes()
|
||||
if !ok || t2 <= t1 {
|
||||
return 0
|
||||
}
|
||||
return (1 - float64(i2-i1)/float64(t2-t1)) * 100
|
||||
}
|
||||
|
||||
func cpuStatic() (model string, cores int) {
|
||||
cores = runtime.NumCPU()
|
||||
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
`HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer k.Close()
|
||||
if s, _, err := k.GetStringValue("ProcessorNameString"); err == nil {
|
||||
model = s
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func kernel() string {
|
||||
v := windows.RtlGetVersion()
|
||||
return fmt.Sprintf("%d.%d.%d", v.MajorVersion, v.MinorVersion, v.BuildNumber)
|
||||
}
|
||||
|
||||
func partitions() []pb.PartitionReport {
|
||||
buf := make([]uint16, 256)
|
||||
n, err := windows.GetLogicalDriveStrings(uint32(len(buf)), &buf[0])
|
||||
if err != nil || n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []pb.PartitionReport
|
||||
for _, root := range splitNullStrings(buf[:n]) {
|
||||
rootPtr, err := windows.UTF16PtrFromString(root)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Fixed disks only: network shares can hang, and removable drives
|
||||
// would appear and vanish between snapshots.
|
||||
if windows.GetDriveType(rootPtr) != windows.DRIVE_FIXED {
|
||||
continue
|
||||
}
|
||||
|
||||
var free, total, totalFree uint64
|
||||
if err := windows.GetDiskFreeSpaceEx(rootPtr, &free, &total, &totalFree); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fsBuf := make([]uint16, 32)
|
||||
var fstype string
|
||||
if err := windows.GetVolumeInformation(rootPtr, nil, 0, nil, nil, nil, &fsBuf[0], uint32(len(fsBuf))); err == nil {
|
||||
fstype = windows.UTF16ToString(fsBuf)
|
||||
}
|
||||
|
||||
out = append(out, pb.PartitionReport{
|
||||
Device: root,
|
||||
Mountpoint: root,
|
||||
Fstype: fstype,
|
||||
TotalBytes: total,
|
||||
UsedBytes: total - totalFree,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// splitNullStrings splits the NUL-separated, double-NUL-terminated block that
|
||||
// GetLogicalDriveStrings writes.
|
||||
func splitNullStrings(b []uint16) []string {
|
||||
var out []string
|
||||
start := 0
|
||||
for i, c := range b {
|
||||
if c != 0 {
|
||||
continue
|
||||
}
|
||||
if i > start {
|
||||
out = append(out, windows.UTF16ToString(b[start:i]))
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user