💥 feat(agent)!: 添加主机监控代理首版

* 实现周期采集、快照缓存、本地 HTTP API 和主动上报

* 补齐 Linux 和 Windows 主机、网络、资源、进程和 TCP 采集

* 添加服务生命周期管理、配置加载、构建脚本和测试覆盖

BREAKING CHANGE: network.ips 拆分为 network.ipv4s 和 network.ipv6s。
This commit is contained in:
codex
2026-05-18 15:25:51 +00:00
commit 89d5e27874
33 changed files with 4205 additions and 0 deletions

View File

@@ -0,0 +1,781 @@
//go:build windows
package collector
import (
"bytes"
"context"
"encoding/csv"
"errors"
"fmt"
"math/bits"
"net"
"os"
"os/exec"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
"mfd-monitor-agent/internal/status"
)
const (
windowsAFInet = 2
windowsAFInet6 = 23
windowsNoError = 0
windowsErrorInsufficientBuffer = 122
windowsTCPTableOwnerPIDListen = 3
windowsTCPStateListen = 2
windowsDriveFixed = 3
windowsDriveRemote = 4
windowsProcessQueryInformation = 0x0400
windowsProcessQueryLimitedInfo = 0x1000
windowsProcessVMRead = 0x0010
windowsTokenQuery = 0x0008
windowsMaxPath = 260
windowsRegSZ = 1
windowsRelationProcessorCore = 0
)
var (
windowsKernel32 = syscall.NewLazyDLL("kernel32.dll")
windowsGlobalMemoryStatusExProc = windowsKernel32.NewProc("GlobalMemoryStatusEx")
windowsGetTickCount64Proc = windowsKernel32.NewProc("GetTickCount64")
windowsGetLogicalDrivesProc = windowsKernel32.NewProc("GetLogicalDrives")
windowsGetDriveTypeWProc = windowsKernel32.NewProc("GetDriveTypeW")
windowsGetDiskFreeSpaceExWProc = windowsKernel32.NewProc("GetDiskFreeSpaceExW")
windowsGetSystemTimesProc = windowsKernel32.NewProc("GetSystemTimes")
windowsGetNativeSystemInfoProc = windowsKernel32.NewProc("GetNativeSystemInfo")
windowsGetLogicalProcessorInfoProc = windowsKernel32.NewProc("GetLogicalProcessorInformation")
windowsQueryFullProcessImageNameWProc = windowsKernel32.NewProc("QueryFullProcessImageNameW")
windowsIphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
windowsGetExtendedTCPTableProc = windowsIphlpapi.NewProc("GetExtendedTcpTable")
windowsPsapi = syscall.NewLazyDLL("psapi.dll")
windowsGetProcessMemoryInfoProc = windowsPsapi.NewProc("GetProcessMemoryInfo")
windowsAdvapi32 = syscall.NewLazyDLL("advapi32.dll")
windowsOpenProcessTokenProc = windowsAdvapi32.NewProc("OpenProcessToken")
windowsGetTokenInformationProc = windowsAdvapi32.NewProc("GetTokenInformation")
windowsLookupAccountSidWProc = windowsAdvapi32.NewProc("LookupAccountSidW")
)
// windowsMemoryStatus mirrors the Windows MEMORYSTATUSEX structure used by GlobalMemoryStatusEx.
type windowsMemoryStatus struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
// windowsMIBTCPRowOwnerPID mirrors a MIB_TCPROW_OWNER_PID row returned by GetExtendedTcpTable.
type windowsMIBTCPRowOwnerPID struct {
State uint32
LocalAddr uint32
LocalPort uint32
RemoteAddr uint32
RemotePort uint32
OwningPID uint32
}
// windowsMIBTCP6RowOwnerPID mirrors a MIB_TCP6ROW_OWNER_PID row returned by GetExtendedTcpTable.
type windowsMIBTCP6RowOwnerPID struct {
State uint32
LocalAddr [16]byte
LocalScopeID uint32
LocalPort uint32
RemoteAddr [16]byte
RemoteScopeID uint32
RemotePort uint32
OwningPID uint32
}
// windowsSystemInfo mirrors the subset of SYSTEM_INFO needed for CPU package sizing.
type windowsSystemInfo struct {
ProcessorArchitecture uint16
Reserved uint16
PageSize uint32
MinimumApplicationAddress uintptr
MaximumApplicationAddress uintptr
ActiveProcessorMask uintptr
NumberOfProcessors uint32
ProcessorType uint32
AllocationGranularity uint32
ProcessorLevel uint16
ProcessorRevision uint16
}
// windowsSystemTimes stores system CPU time counters as 100ns units.
type windowsSystemTimes struct {
Idle uint64
Kernel uint64
User uint64
}
// windowsLogicalProcessorInformation mirrors SYSTEM_LOGICAL_PROCESSOR_INFORMATION for core counting.
type windowsLogicalProcessorInformation struct {
ProcessorMask uintptr
Relationship uint32
Data [16]byte
}
// windowsProcessMemoryCounters mirrors PROCESS_MEMORY_COUNTERS.
type windowsProcessMemoryCounters struct {
CB uint32
PageFaultCount uint32
PeakWorkingSetSize uintptr
WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr
QuotaPeakNonPagedPoolUsage uintptr
QuotaNonPagedPoolUsage uintptr
PagefileUsage uintptr
PeakPagefileUsage uintptr
}
// windowsProcessDetails contains best-effort per-process fields requiring extra handles or commands.
type windowsProcessDetails struct {
Exe string
Cmdline string
Username string
Status string
StartTime int64
CPUPercent float64
MemoryRSS uint64
MemoryVMS uint64
}
// collectSystem fills Windows host identity, OS, and architecture fields.
func collectSystem(_ context.Context, hostID string, out *status.HostStatus) error {
hostname, err := os.Hostname()
if err != nil {
return err
}
out.Host.HostID = hostID
if out.Host.HostID == "" {
out.Host.HostID = hostname
}
out.Host.Hostname = hostname
out.Host.Platform = runtime.GOOS
out.Host.PlatformVersion = windowsVersion()
out.Host.Arch = runtime.GOARCH
out.Host.UptimeSeconds = windowsUptimeSeconds()
return nil
}
// collectNetwork fills Windows interface, address, and MAC data using the portable net package.
func collectNetwork(_ context.Context, out *status.HostStatus) error {
ifaces, err := net.Interfaces()
if err != nil {
return err
}
seenIPs := map[string]struct{}{}
seenMACs := map[string]struct{}{}
for _, iface := range ifaces {
item := status.NetworkInterface{Name: iface.Name, MAC: iface.HardwareAddr.String(), MTU: iface.MTU, Flags: strings.Fields(iface.Flags.String())}
addrs, _ := iface.Addrs()
for _, addr := range addrs {
item.Addresses = append(item.Addresses, addr.String())
seenIPs[addr.String()] = struct{}{}
}
if item.MAC != "" {
seenMACs[item.MAC] = struct{}{}
}
out.Network.Interfaces = append(out.Network.Interfaces, item)
}
out.Network.IPv4s, out.Network.IPv6s = networkAddressSets(seenIPs)
out.Network.MACs = windowsSortedKeys(seenMACs)
return nil
}
// collectResource fills Windows CPU, memory, and disk resource data.
func collectResource(ctx context.Context, out *status.HostStatus) error {
var errs []error
out.CPU.LogicalCores = runtime.NumCPU()
out.CPU.PhysicalCores = windowsPhysicalCores()
out.CPU.ModelName = windowsCPUModelName()
out.CPU.UsagePercent = windowsReadCPUUsagePercent(ctx)
memory, err := windowsReadMemory()
if err != nil {
errs = append(errs, err)
} else {
out.Memory = memory
}
disks, err := windowsReadDisks()
if err != nil {
errs = append(errs, err)
} else {
out.Disks = disks
}
return errors.Join(errs...)
}
// collectProcesses enumerates Windows processes with Toolhelp and returns best-effort names.
func collectProcesses(_ context.Context, out *status.HostStatus) error {
processes, err := windowsProcessList()
out.Processes.Items = processes
if err != nil {
out.Processes.Status = "partial"
return err
}
out.Processes.Status = "ok"
return nil
}
// collectSockets reads Windows TCP listener tables and maps listener PIDs to process names.
func collectSockets(_ context.Context, out *status.HostStatus) error {
processes, processErr := windowsProcessMap()
var errs []error
if processErr != nil {
errs = append(errs, processErr)
}
listeners4, err := windowsTCPListeners(windowsAFInet, "ipv4", processes)
if err != nil {
errs = append(errs, err)
}
listeners6, err := windowsTCPListeners(windowsAFInet6, "ipv6", processes)
if err != nil {
errs = append(errs, err)
}
out.TCPListeners.Items = append(listeners4, listeners6...)
sort.Slice(out.TCPListeners.Items, func(i, j int) bool {
if out.TCPListeners.Items[i].IP == out.TCPListeners.Items[j].IP {
return out.TCPListeners.Items[i].Port < out.TCPListeners.Items[j].Port
}
return out.TCPListeners.Items[i].IP < out.TCPListeners.Items[j].IP
})
if len(errs) > 0 {
out.TCPListeners.Status = "partial"
return errors.Join(errs...)
}
out.TCPListeners.Status = "ok"
return nil
}
// windowsReadMemory calls GlobalMemoryStatusEx and converts physical memory counters to API fields.
func windowsReadMemory() (status.MemoryInfo, error) {
memory := windowsMemoryStatus{Length: uint32(unsafe.Sizeof(windowsMemoryStatus{}))}
ok, _, err := windowsGlobalMemoryStatusExProc.Call(uintptr(unsafe.Pointer(&memory)))
if ok == 0 {
return status.MemoryInfo{}, err
}
return windowsMemoryInfoFromStatus(memory), nil
}
// windowsVersion returns the legacy Windows major.minor.build tuple exposed by GetVersion.
func windowsVersion() string {
version, err := syscall.GetVersion()
if err != nil {
return ""
}
major := byte(version)
minor := uint8(version >> 8)
build := uint16(version >> 16)
return fmt.Sprintf("%d.%d.%d", major, minor, build)
}
// windowsUptimeSeconds returns host uptime in seconds from GetTickCount64.
func windowsUptimeSeconds() uint64 {
millis, _, _ := windowsGetTickCount64Proc.Call()
return uint64(millis) / 1000
}
// windowsPhysicalCores returns physical CPU core count from Windows processor topology.
func windowsPhysicalCores() int {
var size uint32
result, _, _ := windowsGetLogicalProcessorInfoProc.Call(0, uintptr(unsafe.Pointer(&size)))
if result == 0 && size > 0 {
buf := make([]byte, size)
result, _, _ = windowsGetLogicalProcessorInfoProc.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)))
if result != 0 {
entrySize := int(unsafe.Sizeof(windowsLogicalProcessorInformation{}))
cores := 0
for offset := 0; offset+entrySize <= int(size); offset += entrySize {
entry := (*windowsLogicalProcessorInformation)(unsafe.Pointer(&buf[offset]))
if entry.Relationship == windowsRelationProcessorCore {
cores++
}
}
if cores > 0 {
return cores
}
}
}
var info windowsSystemInfo
windowsGetNativeSystemInfoProc.Call(uintptr(unsafe.Pointer(&info)))
if info.NumberOfProcessors > 0 {
return int(info.NumberOfProcessors)
}
if info.ActiveProcessorMask != 0 {
return bits.OnesCount64(uint64(info.ActiveProcessorMask))
}
return runtime.NumCPU()
}
// windowsCPUModelName reads the first processor name from the registry.
func windowsCPUModelName() string {
keyPath, _ := syscall.UTF16PtrFromString(`HARDWARE\DESCRIPTION\System\CentralProcessor\0`)
var key syscall.Handle
if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, keyPath, 0, syscall.KEY_READ, &key); err != nil {
return ""
}
defer syscall.RegCloseKey(key)
valueName, _ := syscall.UTF16PtrFromString("ProcessorNameString")
var valueType uint32
var size uint32
if err := syscall.RegQueryValueEx(key, valueName, nil, &valueType, nil, &size); err != nil || valueType != windowsRegSZ || size == 0 {
return ""
}
buf := make([]byte, size)
if err := syscall.RegQueryValueEx(key, valueName, nil, &valueType, &buf[0], &size); err != nil {
return ""
}
utf16s := make([]uint16, 0, len(buf)/2)
for i := 0; i+1 < int(size); i += 2 {
utf16s = append(utf16s, uint16(buf[i])|uint16(buf[i+1])<<8)
}
return strings.TrimSpace(syscall.UTF16ToString(utf16s))
}
// windowsReadCPUUsagePercent samples GetSystemTimes and returns best-effort CPU usage.
func windowsReadCPUUsagePercent(ctx context.Context) float64 {
prev, err := windowsReadSystemTimes()
if err != nil {
return 0
}
select {
case <-ctx.Done():
return 0
case <-time.After(100 * time.Millisecond):
}
next, err := windowsReadSystemTimes()
if err != nil {
return 0
}
return windowsCPUUsagePercent(prev, next)
}
// windowsReadSystemTimes returns idle, kernel, and user system counters from GetSystemTimes.
func windowsReadSystemTimes() (windowsSystemTimes, error) {
var idle, kernel, user syscall.Filetime
ok, _, err := windowsGetSystemTimesProc.Call(
uintptr(unsafe.Pointer(&idle)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if ok == 0 {
return windowsSystemTimes{}, err
}
return windowsSystemTimes{Idle: windowsFiletimeUint64(idle), Kernel: windowsFiletimeUint64(kernel), User: windowsFiletimeUint64(user)}, nil
}
// windowsCPUUsagePercent calculates non-idle CPU percentage between two Windows CPU samples.
func windowsCPUUsagePercent(prev windowsSystemTimes, next windowsSystemTimes) float64 {
prevTotal := prev.Kernel + prev.User
nextTotal := next.Kernel + next.User
if nextTotal <= prevTotal || next.Idle < prev.Idle {
return 0
}
totalDelta := nextTotal - prevTotal
idleDelta := next.Idle - prev.Idle
if totalDelta == 0 || idleDelta >= totalDelta {
return 0
}
return float64(totalDelta-idleDelta) / float64(totalDelta) * 100
}
// windowsFiletimeUint64 converts a FILETIME to a monotonically comparable counter.
func windowsFiletimeUint64(value syscall.Filetime) uint64 {
return uint64(value.HighDateTime)<<32 | uint64(value.LowDateTime)
}
// windowsReadDisks enumerates fixed and remote Windows drives and returns capacity data.
func windowsReadDisks() ([]status.DiskInfo, error) {
mask, _, _ := windowsGetLogicalDrivesProc.Call()
if mask == 0 {
return nil, syscall.GetLastError()
}
var disks []status.DiskInfo
var errs []error
for i := 0; i < 26; i++ {
if mask&(1<<i) == 0 {
continue
}
root := fmt.Sprintf("%c:\\", 'A'+i)
rootPtr, _ := syscall.UTF16PtrFromString(root)
driveType, _, _ := windowsGetDriveTypeWProc.Call(uintptr(unsafe.Pointer(rootPtr)))
if driveType != windowsDriveFixed && driveType != windowsDriveRemote {
continue
}
var freeAvailable, total, free uint64
ok, _, err := windowsGetDiskFreeSpaceExWProc.Call(
uintptr(unsafe.Pointer(rootPtr)),
uintptr(unsafe.Pointer(&freeAvailable)),
uintptr(unsafe.Pointer(&total)),
uintptr(unsafe.Pointer(&free)),
)
if ok == 0 {
errs = append(errs, fmt.Errorf("%s: %w", root, err))
continue
}
disks = append(disks, windowsDiskInfoFromSpace(root, uint32(driveType), total, freeAvailable))
}
return disks, errors.Join(errs...)
}
// windowsDiskInfoFromSpace converts Windows disk capacity counters into API disk fields.
func windowsDiskInfoFromSpace(root string, driveType uint32, total uint64, free uint64) status.DiskInfo {
used := uint64(0)
if total >= free {
used = total - free
}
percent := 0.0
if total > 0 {
percent = float64(used) / float64(total) * 100
}
return status.DiskInfo{
Device: root, MountPoint: root, FSType: windowsDriveTypeName(driveType),
Unit: "byte", Total: total, Used: used, Free: free, UsedPercent: percent,
}
}
// windowsDriveTypeName maps Windows drive type constants to stable API labels.
func windowsDriveTypeName(driveType uint32) string {
switch driveType {
case windowsDriveFixed:
return "fixed"
case windowsDriveRemote:
return "remote"
default:
return "unknown"
}
}
// windowsMemoryInfoFromStatus converts Windows physical memory counters into HostStatus memory data.
func windowsMemoryInfoFromStatus(memory windowsMemoryStatus) status.MemoryInfo {
used := uint64(0)
if memory.TotalPhys >= memory.AvailPhys {
used = memory.TotalPhys - memory.AvailPhys
}
percent := 0.0
if memory.TotalPhys > 0 {
percent = float64(used) / float64(memory.TotalPhys) * 100
}
return status.MemoryInfo{
Unit: "byte", Total: memory.TotalPhys, Used: used,
Free: memory.AvailPhys, Available: memory.AvailPhys, UsedPercent: percent,
}
}
// windowsTCPListeners returns IPv4 or IPv6 TCP listeners from GetExtendedTcpTable.
func windowsTCPListeners(family int, familyName string, processes map[int]status.ProcessInfo) ([]status.TCPListenerInfo, error) {
table, err := windowsExtendedTCPTable(family)
if err != nil {
return nil, err
}
if len(table) < 4 {
return nil, fmt.Errorf("windows tcp table for %s is too small", familyName)
}
count := *(*uint32)(unsafe.Pointer(&table[0]))
if family == windowsAFInet {
return windowsParseTCP4Rows(table[4:], count, processes), nil
}
return windowsParseTCP6Rows(table[4:], count, processes), nil
}
// windowsExtendedTCPTable calls GetExtendedTcpTable and returns the raw table bytes.
func windowsExtendedTCPTable(family int) ([]byte, error) {
var size uint32
result, _, _ := windowsGetExtendedTCPTableProc.Call(
0,
uintptr(unsafe.Pointer(&size)),
1,
uintptr(family),
uintptr(windowsTCPTableOwnerPIDListen),
0,
)
if result != windowsErrorInsufficientBuffer && result != windowsNoError {
return nil, fmt.Errorf("GetExtendedTcpTable size query failed: %d", result)
}
if size == 0 {
return nil, nil
}
table := make([]byte, size)
result, _, _ = windowsGetExtendedTCPTableProc.Call(
uintptr(unsafe.Pointer(&table[0])),
uintptr(unsafe.Pointer(&size)),
1,
uintptr(family),
uintptr(windowsTCPTableOwnerPIDListen),
0,
)
if result != windowsNoError {
return nil, fmt.Errorf("GetExtendedTcpTable failed: %d", result)
}
return table, nil
}
// windowsParseTCP4Rows converts raw MIB_TCPROW_OWNER_PID rows into listener records.
func windowsParseTCP4Rows(rows []byte, count uint32, processes map[int]status.ProcessInfo) []status.TCPListenerInfo {
rowSize := int(unsafe.Sizeof(windowsMIBTCPRowOwnerPID{}))
items := make([]status.TCPListenerInfo, 0, count)
for i := 0; i < int(count); i++ {
offset := i * rowSize
if offset+rowSize > len(rows) {
break
}
row := *(*windowsMIBTCPRowOwnerPID)(unsafe.Pointer(&rows[offset]))
if row.State != windowsTCPStateListen {
continue
}
process := processes[int(row.OwningPID)]
addr := row.LocalAddr
items = append(items, status.TCPListenerInfo{
IP: net.IPv4(byte(addr), byte(addr>>8), byte(addr>>16), byte(addr>>24)).String(),
Port: windowsTCPPortFromNetworkOrder(row.LocalPort), PID: int(row.OwningPID),
ProcessName: process.Name, ProcessExe: process.Exe, Family: "ipv4",
})
}
return items
}
// windowsParseTCP6Rows converts raw MIB_TCP6ROW_OWNER_PID rows into listener records.
func windowsParseTCP6Rows(rows []byte, count uint32, processes map[int]status.ProcessInfo) []status.TCPListenerInfo {
rowSize := int(unsafe.Sizeof(windowsMIBTCP6RowOwnerPID{}))
items := make([]status.TCPListenerInfo, 0, count)
for i := 0; i < int(count); i++ {
offset := i * rowSize
if offset+rowSize > len(rows) {
break
}
row := *(*windowsMIBTCP6RowOwnerPID)(unsafe.Pointer(&rows[offset]))
if row.State != windowsTCPStateListen {
continue
}
process := processes[int(row.OwningPID)]
items = append(items, status.TCPListenerInfo{
IP: net.IP(row.LocalAddr[:]).String(), Port: windowsTCPPortFromNetworkOrder(row.LocalPort),
PID: int(row.OwningPID), ProcessName: process.Name, ProcessExe: process.Exe, Family: "ipv6",
})
}
return items
}
// windowsTCPPortFromNetworkOrder decodes the low 16 bits Windows stores in network byte order.
func windowsTCPPortFromNetworkOrder(raw uint32) int {
port := uint16(raw)
return int(port>>8) | int(port&0xff)<<8
}
// windowsProcessList enumerates processes with the Windows Toolhelp snapshot API.
func windowsProcessList() ([]status.ProcessInfo, error) {
cmdlines := windowsProcessCommandLines()
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
return nil, err
}
defer syscall.CloseHandle(snapshot)
var entry syscall.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
if err := syscall.Process32First(snapshot, &entry); err != nil {
return nil, err
}
var processes []status.ProcessInfo
for {
details := windowsReadProcessDetails(entry.ProcessID)
if cmdline := cmdlines[int(entry.ProcessID)]; cmdline != "" {
details.Cmdline = cmdline
}
processes = append(processes, windowsProcessInfoFromEntry(entry, details))
err = syscall.Process32Next(snapshot, &entry)
if err != nil {
if errors.Is(err, syscall.ERROR_NO_MORE_FILES) {
return processes, nil
}
return processes, err
}
}
}
// windowsProcessInfoFromEntry combines Toolhelp identity data with optional process details.
func windowsProcessInfoFromEntry(entry syscall.ProcessEntry32, details windowsProcessDetails) status.ProcessInfo {
name := syscall.UTF16ToString(entry.ExeFile[:])
exe := details.Exe
if exe == "" {
exe = name
}
return status.ProcessInfo{
PID: int(entry.ProcessID), PPID: int(entry.ParentProcessID), Name: name,
Exe: exe, Cmdline: details.Cmdline, Username: details.Username, Status: details.Status,
StartTime: details.StartTime, CPUPercent: details.CPUPercent,
MemoryRSS: details.MemoryRSS, MemoryVMS: details.MemoryVMS,
}
}
// windowsReadProcessDetails opens a process and reads fields that may require elevated privileges.
func windowsReadProcessDetails(pid uint32) windowsProcessDetails {
handle, err := syscall.OpenProcess(windowsProcessQueryLimitedInfo|windowsProcessQueryInformation|windowsProcessVMRead, false, pid)
if err != nil {
return windowsProcessDetails{Status: "unknown"}
}
defer syscall.CloseHandle(handle)
details := windowsProcessDetails{Status: "running"}
details.Exe = windowsQueryProcessImageName(handle)
details.Username = windowsQueryProcessUsername(handle)
details.StartTime, details.CPUPercent = windowsQueryProcessTimes(handle)
details.MemoryRSS, details.MemoryVMS = windowsQueryProcessMemory(handle)
return details
}
// windowsQueryProcessImageName returns the full process image path when access allows it.
func windowsQueryProcessImageName(handle syscall.Handle) string {
buf := make([]uint16, windowsMaxPath)
size := uint32(len(buf))
ok, _, _ := windowsQueryFullProcessImageNameWProc.Call(
uintptr(handle),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if ok == 0 || size == 0 {
return ""
}
return syscall.UTF16ToString(buf[:size])
}
// windowsQueryProcessTimes returns start time and lifetime-average CPU percentage.
func windowsQueryProcessTimes(handle syscall.Handle) (int64, float64) {
var creation, exit, kernel, user syscall.Filetime
if err := syscall.GetProcessTimes(handle, &creation, &exit, &kernel, &user); err != nil {
return 0, 0
}
startNanos := creation.Nanoseconds()
elapsedNanos := time.Now().UnixNano() - startNanos
if elapsedNanos <= 0 {
return startNanos / int64(time.Second), 0
}
cpuNanos := int64(windowsFiletimeUint64(kernel)+windowsFiletimeUint64(user)) * 100
percent := float64(cpuNanos) / float64(elapsedNanos) * 100 / float64(runtime.NumCPU())
return startNanos / int64(time.Second), percent
}
// windowsQueryProcessMemory returns working set and pagefile usage when access allows it.
func windowsQueryProcessMemory(handle syscall.Handle) (uint64, uint64) {
counters := windowsProcessMemoryCounters{CB: uint32(unsafe.Sizeof(windowsProcessMemoryCounters{}))}
ok, _, _ := windowsGetProcessMemoryInfoProc.Call(
uintptr(handle),
uintptr(unsafe.Pointer(&counters)),
uintptr(counters.CB),
)
if ok == 0 {
return 0, 0
}
return uint64(counters.WorkingSetSize), uint64(counters.PagefileUsage)
}
// windowsQueryProcessUsername returns DOMAIN\user for a process token when access allows it.
func windowsQueryProcessUsername(handle syscall.Handle) string {
var token syscall.Handle
ok, _, _ := windowsOpenProcessTokenProc.Call(uintptr(handle), uintptr(windowsTokenQuery), uintptr(unsafe.Pointer(&token)))
if ok == 0 {
return ""
}
defer syscall.CloseHandle(token)
var size uint32
windowsGetTokenInformationProc.Call(uintptr(token), 1, 0, 0, uintptr(unsafe.Pointer(&size)))
if size == 0 {
return ""
}
buf := make([]byte, size)
ok, _, _ = windowsGetTokenInformationProc.Call(uintptr(token), 1, uintptr(unsafe.Pointer(&buf[0])), uintptr(size), uintptr(unsafe.Pointer(&size)))
if ok == 0 || len(buf) < int(unsafe.Sizeof(uintptr(0)))+4 {
return ""
}
sid := *(*uintptr)(unsafe.Pointer(&buf[0]))
return windowsLookupAccountName(sid)
}
// windowsLookupAccountName resolves a SID pointer to DOMAIN\name.
func windowsLookupAccountName(sid uintptr) string {
var nameSize, domainSize uint32
var sidType uint32
windowsLookupAccountSidWProc.Call(0, sid, 0, uintptr(unsafe.Pointer(&nameSize)), 0, uintptr(unsafe.Pointer(&domainSize)), uintptr(unsafe.Pointer(&sidType)))
if nameSize == 0 {
return ""
}
name := make([]uint16, nameSize)
domain := make([]uint16, domainSize)
ok, _, _ := windowsLookupAccountSidWProc.Call(
0,
sid,
uintptr(unsafe.Pointer(&name[0])),
uintptr(unsafe.Pointer(&nameSize)),
uintptr(unsafe.Pointer(&domain[0])),
uintptr(unsafe.Pointer(&domainSize)),
uintptr(unsafe.Pointer(&sidType)),
)
if ok == 0 {
return ""
}
account := syscall.UTF16ToString(name[:nameSize])
domainName := syscall.UTF16ToString(domain[:domainSize])
if domainName == "" {
return account
}
return domainName + `\` + account
}
// windowsProcessCommandLines reads command lines in one best-effort WMIC call to avoid per-process shelling.
func windowsProcessCommandLines() map[int]string {
out := map[int]string{}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "wmic", "process", "get", "ProcessId,CommandLine", "/format:csv").Output()
if err != nil {
return out
}
reader := csv.NewReader(bytes.NewReader(output))
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
return out
}
for _, record := range records {
if len(record) < 3 || strings.EqualFold(record[1], "CommandLine") {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(record[2]))
if err != nil {
continue
}
out[pid] = strings.TrimSpace(record[1])
}
return out
}
// windowsProcessMap returns process metadata keyed by PID for TCP owner mapping.
func windowsProcessMap() (map[int]status.ProcessInfo, error) {
processes, err := windowsProcessList()
out := make(map[int]status.ProcessInfo, len(processes))
for _, process := range processes {
out[process.PID] = process
}
return out, err
}
// windowsSortedKeys returns deterministic sorted keys from a string set.
func windowsSortedKeys(values map[string]struct{}) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}