* 默认监听调整为 0.0.0.0:8080,并要求查询参数 token * 支持首次 run 自动生成含随机 token 的配置文件 * 补充 HTTP 鉴权、配置校验和相关测试 * 完善代码注释,覆盖缓存、上报、服务和平台采集路径 * 新增架构与开发流程文档,补充一键构建脚本
816 lines
28 KiB
Go
816 lines
28 KiB
Go
//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"
|
||
)
|
||
|
||
// Windows API 常量保持与 Win32 头文件一致,用于 syscall 调用和结果解析。
|
||
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 (
|
||
// 下列 LazyDLL/Proc 是 Windows 原生 API 入口,按需加载以保持单二进制无外部包装依赖。
|
||
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 镜像 GlobalMemoryStatusEx 使用的 Windows MEMORYSTATUSEX 结构。
|
||
// 字段顺序和宽度必须匹配 Win32 ABI,不应随 Go 风格重排。
|
||
type windowsMemoryStatus struct {
|
||
// Length 必须在调用前设置为结构体大小。
|
||
Length uint32
|
||
// MemoryLoad 是 Windows 返回的内存使用率百分比。
|
||
MemoryLoad uint32
|
||
// TotalPhys 和 AvailPhys 是物理内存总量与可用量,单位为字节。
|
||
TotalPhys uint64
|
||
AvailPhys uint64
|
||
TotalPageFile uint64
|
||
AvailPageFile uint64
|
||
TotalVirtual uint64
|
||
AvailVirtual uint64
|
||
AvailExtendedVirtual uint64
|
||
}
|
||
|
||
// windowsMIBTCPRowOwnerPID 镜像 GetExtendedTcpTable 返回的 MIB_TCPROW_OWNER_PID 行。
|
||
// 字段布局必须与 iphlpapi 的 IPv4 TCP 表行一致。
|
||
type windowsMIBTCPRowOwnerPID struct {
|
||
State uint32
|
||
// LocalAddr 和 LocalPort 使用 Windows API 的原始网络字节序表示。
|
||
LocalAddr uint32
|
||
LocalPort uint32
|
||
RemoteAddr uint32
|
||
RemotePort uint32
|
||
OwningPID uint32
|
||
}
|
||
|
||
// windowsMIBTCP6RowOwnerPID 镜像 GetExtendedTcpTable 返回的 MIB_TCP6ROW_OWNER_PID 行。
|
||
// 字段布局必须与 iphlpapi 的 IPv6 TCP 表行一致。
|
||
type windowsMIBTCP6RowOwnerPID struct {
|
||
State uint32
|
||
// LocalAddr 是网络序 IPv6 地址字节。
|
||
LocalAddr [16]byte
|
||
LocalScopeID uint32
|
||
LocalPort uint32
|
||
RemoteAddr [16]byte
|
||
RemoteScopeID uint32
|
||
RemotePort uint32
|
||
OwningPID uint32
|
||
}
|
||
|
||
// windowsSystemInfo 镜像 CPU 封装规模计算所需的 SYSTEM_INFO 子集。
|
||
// 字段顺序必须匹配 Win32 ABI,当前仅使用处理器数量与掩码字段。
|
||
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 以 100 纳秒为单位存储系统 CPU 时间计数。
|
||
type windowsSystemTimes struct {
|
||
Idle uint64
|
||
Kernel uint64
|
||
User uint64
|
||
}
|
||
|
||
// windowsLogicalProcessorInformation 镜像用于统计核心的 SYSTEM_LOGICAL_PROCESSOR_INFORMATION。
|
||
// Data 只作为占位保留 ABI 大小,当前仅读取 Relationship。
|
||
type windowsLogicalProcessorInformation struct {
|
||
ProcessorMask uintptr
|
||
Relationship uint32
|
||
Data [16]byte
|
||
}
|
||
|
||
// windowsProcessMemoryCounters 镜像 PROCESS_MEMORY_COUNTERS。
|
||
// 字段宽度依赖平台指针大小,必须保持与 psapi 返回结构一致。
|
||
type windowsProcessMemoryCounters struct {
|
||
CB uint32
|
||
PageFaultCount uint32
|
||
PeakWorkingSetSize uintptr
|
||
WorkingSetSize uintptr
|
||
QuotaPeakPagedPoolUsage uintptr
|
||
QuotaPagedPoolUsage uintptr
|
||
QuotaPeakNonPagedPoolUsage uintptr
|
||
QuotaNonPagedPoolUsage uintptr
|
||
PagefileUsage uintptr
|
||
PeakPagefileUsage uintptr
|
||
}
|
||
|
||
// windowsProcessDetails 包含需额外句柄或命令才能获取的尽力进程字段。
|
||
type windowsProcessDetails struct {
|
||
// Exe 是完整映像路径;权限不足时为空。
|
||
Exe string
|
||
// Cmdline 是命令行文本;WMIC 不可用或超时时为空。
|
||
Cmdline string
|
||
// Username 是 DOMAIN\user;令牌读取失败时为空。
|
||
Username string
|
||
Status string
|
||
// StartTime 是 Unix 秒级时间戳。
|
||
StartTime int64
|
||
// CPUPercent 是生命周期平均 CPU 占用百分比。
|
||
CPUPercent float64
|
||
// MemoryRSS 和 MemoryVMS 单位为字节。
|
||
MemoryRSS uint64
|
||
MemoryVMS uint64
|
||
}
|
||
|
||
// collectSystem 填充 Windows 主机标识、操作系统与架构字段。
|
||
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 使用可移植 net 包填充 Windows 网卡、地址与 MAC 数据。
|
||
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 填充 Windows CPU、内存与磁盘资源数据。
|
||
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 {
|
||
// 内存失败不阻止 CPU/磁盘字段返回,交由 snapshot 标记 partial。
|
||
errs = append(errs, err)
|
||
} else {
|
||
out.Memory = memory
|
||
}
|
||
disks, err := windowsReadDisks()
|
||
if err != nil {
|
||
// 单个资源子项失败通过 joined error 上报,保留其他成功资源。
|
||
errs = append(errs, err)
|
||
} else {
|
||
out.Disks = disks
|
||
}
|
||
return errors.Join(errs...)
|
||
}
|
||
|
||
// collectProcesses 通过 Toolhelp 枚举 Windows 进程并返回尽力获取的名称等信息。
|
||
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 读取 Windows TCP 监听表并将监听 PID 映射到进程名。
|
||
func collectSockets(_ context.Context, out *status.HostStatus) error {
|
||
processes, processErr := windowsProcessMap()
|
||
var errs []error
|
||
if processErr != nil {
|
||
// 进程元数据失败时仍可返回 TCP 表,只是进程名/路径映射可能为空。
|
||
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 调用 GlobalMemoryStatusEx 并将物理内存计数转换为 API 字段。
|
||
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 返回 GetVersion 暴露的传统 major.minor.build 版本元组。
|
||
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 从 GetTickCount64 返回主机运行时长(秒)。
|
||
func windowsUptimeSeconds() uint64 {
|
||
millis, _, _ := windowsGetTickCount64Proc.Call()
|
||
return uint64(millis) / 1000
|
||
}
|
||
|
||
// windowsPhysicalCores 从 Windows 处理器拓扑返回物理 CPU 核心数。
|
||
func windowsPhysicalCores() int {
|
||
var size uint32
|
||
// Windows 典型模式:先查询所需缓冲区大小,再用正确大小重新调用。
|
||
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
|
||
}
|
||
}
|
||
}
|
||
// 老系统或 API 失败时退回到 SYSTEM_INFO,再退回 runtime.NumCPU。
|
||
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 从注册表读取首个处理器名称。
|
||
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 采样 GetSystemTimes 并返回尽力估算的 CPU 使用率。
|
||
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 从 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 根据两次 Windows CPU 采样计算非空闲 CPU 百分比。
|
||
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 将 FILETIME 转换为可单调比较的计数。
|
||
func windowsFiletimeUint64(value syscall.Filetime) uint64 {
|
||
return uint64(value.HighDateTime)<<32 | uint64(value.LowDateTime)
|
||
}
|
||
|
||
// windowsReadDisks 枚举 Windows 本地固定盘与远程盘并返回容量数据。
|
||
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 {
|
||
// 个别盘符失败不影响其他磁盘,错误汇总后让快照降级为 partial。
|
||
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 将 Windows 磁盘容量计数转换为 API 磁盘字段。
|
||
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 将 Windows 驱动器类型常量映射为稳定的 API 标签。
|
||
func windowsDriveTypeName(driveType uint32) string {
|
||
switch driveType {
|
||
case windowsDriveFixed:
|
||
return "fixed"
|
||
case windowsDriveRemote:
|
||
return "remote"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// windowsMemoryInfoFromStatus 将 Windows 物理内存计数转换为 HostStatus 内存数据。
|
||
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 从 GetExtendedTcpTable 返回 IPv4 或 IPv6 TCP 监听项。
|
||
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 调用 GetExtendedTcpTable 并返回原始表字节。
|
||
func windowsExtendedTCPTable(family int) ([]byte, error) {
|
||
var size uint32
|
||
// 第一次调用用于获取所需缓冲区大小,ERROR_INSUFFICIENT_BUFFER 是预期结果。
|
||
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 将原始 MIB_TCPROW_OWNER_PID 行转换为监听记录。
|
||
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) {
|
||
// 防御损坏或截断的 API 返回,避免 unsafe 读取越界。
|
||
break
|
||
}
|
||
row := *(*windowsMIBTCPRowOwnerPID)(unsafe.Pointer(&rows[offset]))
|
||
if row.State != windowsTCPStateListen {
|
||
continue
|
||
}
|
||
process := processes[int(row.OwningPID)]
|
||
// PID 映射缺失时 process 为零值,仍返回端口本身供调用方排障。
|
||
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 将原始 MIB_TCP6ROW_OWNER_PID 行转换为监听记录。
|
||
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) {
|
||
// 防御损坏或截断的 API 返回,避免 unsafe 读取越界。
|
||
break
|
||
}
|
||
row := *(*windowsMIBTCP6RowOwnerPID)(unsafe.Pointer(&rows[offset]))
|
||
if row.State != windowsTCPStateListen {
|
||
continue
|
||
}
|
||
process := processes[int(row.OwningPID)]
|
||
// PID 映射缺失时 process 为零值,仍返回端口本身供调用方排障。
|
||
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 解码 Windows 以网络字节序存储的低 16 位端口。
|
||
func windowsTCPPortFromNetworkOrder(raw uint32) int {
|
||
port := uint16(raw)
|
||
return int(port>>8) | int(port&0xff)<<8
|
||
}
|
||
|
||
// windowsProcessList 使用 Windows Toolhelp 快照 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 合并 Toolhelp 身份数据与可选进程详情。
|
||
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 打开进程并读取可能需要提升权限的字段。
|
||
func windowsReadProcessDetails(pid uint32) windowsProcessDetails {
|
||
handle, err := syscall.OpenProcess(windowsProcessQueryLimitedInfo|windowsProcessQueryInformation|windowsProcessVMRead, false, pid)
|
||
if err != nil {
|
||
// 系统进程或权限不足时无法打开句柄,调用方仍保留 Toolhelp 的基础进程记录。
|
||
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 在访问允许时返回完整进程映像路径。
|
||
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 返回启动时间与生命周期平均 CPU 百分比。
|
||
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 在访问允许时返回工作集与页面文件占用。
|
||
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 在访问允许时返回进程令牌的 DOMAIN\user。
|
||
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 将 SID 指针解析为 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 通过一次尽力 WMIC 调用读取命令行,避免逐进程 shell。
|
||
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 {
|
||
// WMIC 在新系统上可能不可用;命令行字段按空值降级。
|
||
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 返回以 PID 为键的进程元数据,供 TCP 属主映射使用。
|
||
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 从字符串集合返回确定顺序的排序键列表。
|
||
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
|
||
}
|