✨ feat(agent): 添加 token 鉴权和流程文档

* 默认监听调整为 0.0.0.0:8080,并要求查询参数 token
* 支持首次 run 自动生成含随机 token 的配置文件
* 补充 HTTP 鉴权、配置校验和相关测试
* 完善代码注释,覆盖缓存、上报、服务和平台采集路径
* 新增架构与开发流程文档,补充一键构建脚本
This commit is contained in:
codex
2026-05-22 06:46:21 +00:00
parent 877e4cf41d
commit de3fd88948
19 changed files with 875 additions and 125 deletions

View File

@@ -23,6 +23,7 @@ import (
"mfd-monitor-agent/internal/status"
)
// Windows API 常量保持与 Win32 头文件一致,用于 syscall 调用和结果解析。
const (
windowsAFInet = 2
windowsAFInet6 = 23
@@ -42,6 +43,7 @@ const (
)
var (
// 下列 LazyDLL/Proc 是 Windows 原生 API 入口,按需加载以保持单二进制无外部包装依赖。
windowsKernel32 = syscall.NewLazyDLL("kernel32.dll")
windowsGlobalMemoryStatusExProc = windowsKernel32.NewProc("GlobalMemoryStatusEx")
windowsGetTickCount64Proc = windowsKernel32.NewProc("GetTickCount64")
@@ -63,9 +65,13 @@ var (
)
// windowsMemoryStatus 镜像 GlobalMemoryStatusEx 使用的 Windows MEMORYSTATUSEX 结构。
// 字段顺序和宽度必须匹配 Win32 ABI,不应随 Go 风格重排。
type windowsMemoryStatus struct {
Length uint32
MemoryLoad uint32
// Length 必须在调用前设置为结构体大小。
Length uint32
// MemoryLoad 是 Windows 返回的内存使用率百分比。
MemoryLoad uint32
// TotalPhys 和 AvailPhys 是物理内存总量与可用量,单位为字节。
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
@@ -76,8 +82,10 @@ type windowsMemoryStatus struct {
}
// windowsMIBTCPRowOwnerPID 镜像 GetExtendedTcpTable 返回的 MIB_TCPROW_OWNER_PID 行。
// 字段布局必须与 iphlpapi 的 IPv4 TCP 表行一致。
type windowsMIBTCPRowOwnerPID struct {
State uint32
State uint32
// LocalAddr 和 LocalPort 使用 Windows API 的原始网络字节序表示。
LocalAddr uint32
LocalPort uint32
RemoteAddr uint32
@@ -86,8 +94,10 @@ type windowsMIBTCPRowOwnerPID struct {
}
// windowsMIBTCP6RowOwnerPID 镜像 GetExtendedTcpTable 返回的 MIB_TCP6ROW_OWNER_PID 行。
// 字段布局必须与 iphlpapi 的 IPv6 TCP 表行一致。
type windowsMIBTCP6RowOwnerPID struct {
State uint32
State uint32
// LocalAddr 是网络序 IPv6 地址字节。
LocalAddr [16]byte
LocalScopeID uint32
LocalPort uint32
@@ -98,6 +108,7 @@ type windowsMIBTCP6RowOwnerPID struct {
}
// windowsSystemInfo 镜像 CPU 封装规模计算所需的 SYSTEM_INFO 子集。
// 字段顺序必须匹配 Win32 ABI,当前仅使用处理器数量与掩码字段。
type windowsSystemInfo struct {
ProcessorArchitecture uint16
Reserved uint16
@@ -120,6 +131,7 @@ type windowsSystemTimes struct {
}
// windowsLogicalProcessorInformation 镜像用于统计核心的 SYSTEM_LOGICAL_PROCESSOR_INFORMATION。
// Data 只作为占位保留 ABI 大小,当前仅读取 Relationship。
type windowsLogicalProcessorInformation struct {
ProcessorMask uintptr
Relationship uint32
@@ -127,6 +139,7 @@ type windowsLogicalProcessorInformation struct {
}
// windowsProcessMemoryCounters 镜像 PROCESS_MEMORY_COUNTERS。
// 字段宽度依赖平台指针大小,必须保持与 psapi 返回结构一致。
type windowsProcessMemoryCounters struct {
CB uint32
PageFaultCount uint32
@@ -142,14 +155,20 @@ type windowsProcessMemoryCounters struct {
// windowsProcessDetails 包含需额外句柄或命令才能获取的尽力进程字段。
type windowsProcessDetails struct {
Exe string
Cmdline string
Username string
Status string
StartTime int64
// Exe 是完整映像路径;权限不足时为空。
Exe string
// Cmdline 是命令行文本;WMIC 不可用或超时时为空。
Cmdline string
// Username 是 DOMAIN\user;令牌读取失败时为空。
Username string
Status string
// StartTime 是 Unix 秒级时间戳。
StartTime int64
// CPUPercent 是生命周期平均 CPU 占用百分比。
CPUPercent float64
MemoryRSS uint64
MemoryVMS uint64
// MemoryRSS 和 MemoryVMS 单位为字节。
MemoryRSS uint64
MemoryVMS uint64
}
// collectSystem 填充 Windows 主机标识、操作系统与架构字段。
@@ -204,12 +223,14 @@ func collectResource(ctx context.Context, out *status.HostStatus) error {
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
@@ -234,6 +255,7 @@ 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)
@@ -290,6 +312,7 @@ func windowsUptimeSeconds() uint64 {
// 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)
@@ -308,6 +331,7 @@ func windowsPhysicalCores() int {
}
}
}
// 老系统或 API 失败时退回到 SYSTEM_INFO,再退回 runtime.NumCPU。
var info windowsSystemInfo
windowsGetNativeSystemInfoProc.Call(uintptr(unsafe.Pointer(&info)))
if info.NumberOfProcessors > 0 {
@@ -412,6 +436,7 @@ func windowsReadDisks() ([]status.DiskInfo, error) {
rootPtr, _ := syscall.UTF16PtrFromString(root)
driveType, _, _ := windowsGetDriveTypeWProc.Call(uintptr(unsafe.Pointer(rootPtr)))
if driveType != windowsDriveFixed && driveType != windowsDriveRemote {
// 首版只暴露固定盘和网络盘,跳过光驱、可移动盘等不稳定介质。
continue
}
var freeAvailable, total, free uint64
@@ -422,6 +447,7 @@ func windowsReadDisks() ([]status.DiskInfo, error) {
uintptr(unsafe.Pointer(&free)),
)
if ok == 0 {
// 个别盘符失败不影响其他磁盘,错误汇总后让快照降级为 partial。
errs = append(errs, fmt.Errorf("%s: %w", root, err))
continue
}
@@ -493,6 +519,7 @@ func windowsTCPListeners(family int, familyName string, processes map[int]status
// windowsExtendedTCPTable 调用 GetExtendedTcpTable 并返回原始表字节。
func windowsExtendedTCPTable(family int) ([]byte, error) {
var size uint32
// 第一次调用用于获取所需缓冲区大小,ERROR_INSUFFICIENT_BUFFER 是预期结果。
result, _, _ := windowsGetExtendedTCPTableProc.Call(
0,
uintptr(unsafe.Pointer(&size)),
@@ -508,6 +535,7 @@ func windowsExtendedTCPTable(family int) ([]byte, error) {
return nil, nil
}
table := make([]byte, size)
// 第二次调用用已分配缓冲区读取包含行数和行数据的原始表。
result, _, _ = windowsGetExtendedTCPTableProc.Call(
uintptr(unsafe.Pointer(&table[0])),
uintptr(unsafe.Pointer(&size)),
@@ -529,6 +557,7 @@ func windowsParseTCP4Rows(rows []byte, count uint32, processes map[int]status.Pr
for i := 0; i < int(count); i++ {
offset := i * rowSize
if offset+rowSize > len(rows) {
// 防御损坏或截断的 API 返回,避免 unsafe 读取越界。
break
}
row := *(*windowsMIBTCPRowOwnerPID)(unsafe.Pointer(&rows[offset]))
@@ -536,6 +565,7 @@ func windowsParseTCP4Rows(rows []byte, count uint32, processes map[int]status.Pr
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(),
@@ -553,6 +583,7 @@ func windowsParseTCP6Rows(rows []byte, count uint32, processes map[int]status.Pr
for i := 0; i < int(count); i++ {
offset := i * rowSize
if offset+rowSize > len(rows) {
// 防御损坏或截断的 API 返回,避免 unsafe 读取越界。
break
}
row := *(*windowsMIBTCP6RowOwnerPID)(unsafe.Pointer(&rows[offset]))
@@ -560,6 +591,7 @@ func windowsParseTCP6Rows(rows []byte, count uint32, processes map[int]status.Pr
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",
@@ -623,6 +655,7 @@ func windowsProcessInfoFromEntry(entry syscall.ProcessEntry32, details windowsPr
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)
@@ -739,6 +772,7 @@ func windowsProcessCommandLines() map[int]string {
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))