Files
mfd-monitor-agent/internal/collector/collector_windows.go
codex de3fd88948 ✨ feat(agent): 添加 token 鉴权和流程文档
* 默认监听调整为 0.0.0.0:8080,并要求查询参数 token
* 支持首次 run 自动生成含随机 token 的配置文件
* 补充 HTTP 鉴权、配置校验和相关测试
* 完善代码注释,覆盖缓存、上报、服务和平台采集路径
* 新增架构与开发流程文档,补充一键构建脚本
2026-05-22 06:46:21 +00:00

816 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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
}