Compare commits
2 Commits
89d5e27874
...
de3fd88948
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3fd88948 | ||
|
|
877e4cf41d |
14
AGENTS.md
14
AGENTS.md
@@ -6,9 +6,10 @@
|
||||
|
||||
- MFD Monitor Agent 是一个使用 Go 实现的单二进制主机状态监控 agent。
|
||||
- agent 需要支持 Windows 和 Linux,用于采集并暴露完整主机状态。
|
||||
- agent 默认通过本地 HTTP API 提供查询能力,默认监听 `127.0.0.1:8080`。
|
||||
- `GET /health` 用于返回进程存活状态、版本信息、最近快照时间和最近采集状态。
|
||||
- `GET /status` 用于返回缓存中的完整 `HostStatus` 主机状态快照。
|
||||
- agent 默认通过 HTTP API 提供查询能力,默认监听 `0.0.0.0:8080`。
|
||||
- `GET /health?token=` 用于返回进程存活状态、版本信息、最近快照时间和最近采集状态。
|
||||
- `GET /status?token=` 用于返回缓存中的完整 `HostStatus` 主机状态快照。
|
||||
- `http.token` 配置在 JSON 中;`agent run` 时若工作目录无 `config.json` 则自动生成含随机 token 的默认文件。
|
||||
- agent 通过后台周期采集刷新内存缓存,HTTP 接口和主动上报都读取同一份缓存。
|
||||
- agent 支持可选的周期性 HTTP POST 主动上报,上报数据必须复用 `/status` 的 `HostStatus` JSON 结构。
|
||||
- agent 需要通过 CLI 管理服务生命周期,包括 `run`、`install`、`uninstall`、`start`、`stop` 和 `version`。
|
||||
@@ -19,7 +20,7 @@
|
||||
- 安装阶段必须要求管理员/root 权限。
|
||||
- 首版运行阶段默认使用高权限服务账户,以稳定采集完整进程列表和 TCP 监听端口到进程的映射。
|
||||
- 接口层必须支持部分成功:某些字段或子采集器失败时,不应导致整个 `/status` 失败,应返回 `partial` 或具体错误原因。
|
||||
- 默认监听地址必须尽量收敛到本地地址 `127.0.0.1`,避免默认暴露敏感主机信息。
|
||||
- 默认监听 `0.0.0.0:8080`,必须通过查询参数 `?token=` 鉴权;`http.token` 不能为空。
|
||||
|
||||
### 架构要求
|
||||
|
||||
@@ -35,7 +36,7 @@
|
||||
- `httpapi`:提供 `GET /health` 和 `GET /status`。
|
||||
- `reporter`:定时 POST `HostStatus` 到中心端。
|
||||
- `service`:封装 Windows/Linux 服务生命周期和安装命令。
|
||||
- `config`:统一加载监听地址、采集周期、上报周期、上报目标、机器标识等配置。
|
||||
- `config`:统一加载监听地址、HTTP token、采集周期、上报周期、上报目标、机器标识等配置。
|
||||
|
||||
### HostStatus 要求
|
||||
|
||||
@@ -95,7 +96,8 @@
|
||||
### 首版边界
|
||||
|
||||
- 首版默认包含 IP、MAC、主机名、CPU、内存、磁盘、完整进程列表、TCP 监听端口及进程映射、本地 HTTP 查询、主动 HTTP 上报、Windows/Linux 服务化。
|
||||
- 首版默认不包含历史存储、趋势分析、分页查询、UDP 监听信息、线程列表、进程句柄、连接状态详情、强制 Token/mTLS。
|
||||
- 首版默认不包含历史存储、趋势分析、分页查询、UDP 监听信息、线程列表、进程句柄、连接状态详情、Bearer Token/mTLS。
|
||||
- 首版 HTTP API 使用查询参数 token 鉴权(非 Bearer/mTLS)。
|
||||
|
||||
## 注释规则
|
||||
|
||||
|
||||
51
README.md
51
README.md
@@ -4,13 +4,34 @@ Single-binary host status agent implemented in Go.
|
||||
|
||||
## Features
|
||||
|
||||
- `GET /health` returns process liveness and latest snapshot metadata.
|
||||
- `GET /status` returns the cached full host snapshot.
|
||||
- `GET /health?token=...` returns process liveness and latest snapshot metadata.
|
||||
- `GET /status?token=...` returns the cached full host snapshot.
|
||||
- On first `agent run`, creates `config.json` in the working directory if missing (with a random token).
|
||||
- Periodic background collection keeps HTTP responses fast.
|
||||
- Optional periodic HTTP POST reporting reuses the same `HostStatus` JSON.
|
||||
- Linux collectors read host, network, CPU, memory, disk, process, and TCP listener data from the OS.
|
||||
- Windows and Linux service lifecycle commands are exposed through the CLI.
|
||||
|
||||
**完整部署说明(中文):** [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
|
||||
|
||||
**架构与流程说明:** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
||||
|
||||
**开发与构建流程:** [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
go run ./cmd/agent run
|
||||
```
|
||||
|
||||
首次运行会在当前目录生成 `config.json`,其中包含随机 `http.token`。使用该 token 访问:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8080/health?token=<http.token>
|
||||
http://127.0.0.1:8080/status?token=<http.token>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
@@ -24,6 +45,12 @@ agent version
|
||||
|
||||
## Build
|
||||
|
||||
All default targets:
|
||||
|
||||
```bash
|
||||
./scripts/build-all.sh
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
@@ -46,13 +73,14 @@ Build outputs are written to `dist/`, which is intentionally ignored by Git.
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings have safe defaults. The HTTP API listens on `127.0.0.1:8080` by default.
|
||||
Example config: [configs/example.json](configs/example.json). See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for defaults, environment variables, and production deployment steps.
|
||||
|
||||
```json
|
||||
{
|
||||
"host_id": "host-a",
|
||||
"http": {
|
||||
"listen_address": "127.0.0.1:8080"
|
||||
"listen_address": "0.0.0.0:8080",
|
||||
"token": "replace-with-a-strong-random-value"
|
||||
},
|
||||
"collect": {
|
||||
"interval": "30s",
|
||||
@@ -67,13 +95,28 @@ All settings have safe defaults. The HTTP API listens on `127.0.0.1:8080` by def
|
||||
}
|
||||
```
|
||||
|
||||
API access (browser-friendly query parameter):
|
||||
|
||||
```text
|
||||
http://<host>:8080/health?token=<http.token>
|
||||
http://<host>:8080/status?token=<http.token>
|
||||
```
|
||||
|
||||
Environment overrides:
|
||||
|
||||
- `MFD_AGENT_HOST_ID`
|
||||
- `MFD_AGENT_LISTEN`
|
||||
- `MFD_AGENT_TOKEN`
|
||||
- `MFD_AGENT_COLLECT_INTERVAL`
|
||||
- `MFD_AGENT_COLLECT_TIMEOUT`
|
||||
- `MFD_AGENT_REPORT_ENABLED`
|
||||
- `MFD_AGENT_REPORT_ENDPOINT`
|
||||
- `MFD_AGENT_REPORT_INTERVAL`
|
||||
- `MFD_AGENT_REPORT_TIMEOUT`
|
||||
|
||||
## Documentation Map
|
||||
|
||||
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md): production deployment, validation, upgrade, rollback, troubleshooting.
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): runtime workflow, cache behavior, partial-success model, platform differences.
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): tests, build scripts, cross-platform verification.
|
||||
- [AGENTS.md](AGENTS.md): repository rules and first-version scope.
|
||||
|
||||
@@ -20,9 +20,10 @@ import (
|
||||
"mfd-monitor-agent/internal/snapshot"
|
||||
)
|
||||
|
||||
const version = "0.1.0"
|
||||
// version 是编译时可通过 -ldflags 覆盖的 agent 版本号。
|
||||
var version = "0.1.0"
|
||||
|
||||
// main runs the CLI and exits non-zero when a command fails.
|
||||
// main 运行 CLI,命令失败时以非零状态退出。
|
||||
func main() {
|
||||
if err := runCLI(os.Args[1:]); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
@@ -30,7 +31,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// runCLI dispatches agent subcommands and loads config before invoking command handlers.
|
||||
// runCLI 分发 agent 子命令,并在调用处理函数前加载配置。
|
||||
func runCLI(args []string) error {
|
||||
if len(args) == 0 {
|
||||
args = []string{"run"}
|
||||
@@ -38,11 +39,12 @@ func runCLI(args []string) error {
|
||||
switch args[0] {
|
||||
case "run":
|
||||
fs := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
configPath := fs.String("config", "", "path to JSON config file")
|
||||
configPath := fs.String("config", "", "JSON 配置文件路径")
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(*configPath)
|
||||
path := config.ResolvePath(*configPath)
|
||||
cfg, err := config.LoadOrCreate(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,29 +81,31 @@ func runCLI(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// commandConfig parses service command flags and resolves the executable path.
|
||||
// commandConfig 解析服务相关子命令的标志并解析可执行文件路径。
|
||||
func commandConfig(args []string) (config.Config, string, string, error) {
|
||||
fs := flag.NewFlagSet("service", flag.ExitOnError)
|
||||
configPath := fs.String("config", "", "path to JSON config file")
|
||||
executableFlag := fs.String("exe", "", "path to agent executable")
|
||||
configPath := fs.String("config", "", "JSON 配置文件路径")
|
||||
executableFlag := fs.String("exe", "", "agent 可执行文件路径(默认当前二进制)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return config.Config{}, "", "", err
|
||||
}
|
||||
cfg, err := config.Load(*configPath)
|
||||
path := config.ResolvePath(*configPath)
|
||||
cfg, err := config.Load(path)
|
||||
if err != nil {
|
||||
return cfg, "", "", err
|
||||
}
|
||||
executable := *executableFlag
|
||||
if executable == "" {
|
||||
// 服务安装需要写入一个稳定的二进制路径;未显式传入时使用当前进程路径。
|
||||
executable, err = os.Executable()
|
||||
if err != nil {
|
||||
return cfg, "", "", err
|
||||
}
|
||||
}
|
||||
return cfg, executable, *configPath, nil
|
||||
return cfg, executable, path, nil
|
||||
}
|
||||
|
||||
// runAgent starts collection, optional reporting, and the local HTTP API until signaled.
|
||||
// runAgent 启动采集、可选上报与本地 HTTP API,直至收到退出信号。
|
||||
func runAgent(cfg config.Config) error {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
@@ -109,20 +113,23 @@ func runAgent(cfg config.Config) error {
|
||||
|
||||
startedAt := time.Now().UTC()
|
||||
cache := snapshot.NewCache(collector.New(cfg.HostID), version, startedAt)
|
||||
// 采集在后台立即执行一次并周期刷新,HTTP 与上报都只读取这份缓存。
|
||||
go cache.Run(ctx, cfg.Collect.Interval.Duration(), cfg.Collect.Timeout.Duration())
|
||||
|
||||
if cfg.Report.Enabled {
|
||||
client := reporter.NewClient(cfg.Report.Endpoint, cfg.Report.Timeout.Duration())
|
||||
// 上报循环独立于 HTTP 服务运行;失败只记录日志并重试,不终止 agent。
|
||||
go reporter.Run(ctx, cache, client, cfg.Report.Interval.Duration(), logger)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.HTTP.ListenAddress,
|
||||
Handler: httpapi.New(cache),
|
||||
Handler: httpapi.New(cache, cfg.HTTP.Token),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// 收到系统信号后给 HTTP 请求一个短暂宽限期,避免直接中断正在返回的快照。
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(shutdownCtx)
|
||||
|
||||
22
configs/example.json
Normal file
22
configs/example.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"host_id": "host-example",
|
||||
"http": {
|
||||
"listen_address": "0.0.0.0:8080",
|
||||
"token": "请替换为强随机值"
|
||||
},
|
||||
"collect": {
|
||||
"interval": "30s",
|
||||
"timeout": "10s"
|
||||
},
|
||||
"report": {
|
||||
"enabled": false,
|
||||
"endpoint": "http://center.example.internal/agent/status",
|
||||
"interval": "60s",
|
||||
"timeout": "5s"
|
||||
},
|
||||
"service": {
|
||||
"name": "mfd-monitor-agent",
|
||||
"display_name": "MFD Monitor Agent",
|
||||
"description": "采集主机状态并通过 HTTP 提供查询"
|
||||
}
|
||||
}
|
||||
172
docs/ARCHITECTURE.md
Normal file
172
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# MFD Monitor Agent 架构与工作流程
|
||||
|
||||
本文档汇总当前代码中的实际运行流程,供开发、排障和评审时快速理解 agent 如何从配置加载到采集、缓存、查询、上报和服务化运行。
|
||||
|
||||
## 进程职责
|
||||
|
||||
MFD Monitor Agent 采用单进程一体化架构。一个 agent 进程同时负责:
|
||||
|
||||
- 周期采集主机状态
|
||||
- 保存最近一次 `HostStatus` 内存缓存
|
||||
- 提供本地 HTTP API
|
||||
- 可选地向中心端主动 POST 上报
|
||||
- 执行 Linux/Windows 服务安装、启动、停止和卸载命令
|
||||
|
||||
HTTP 查询与主动上报读取同一份缓存,不在请求路径上触发实时系统扫描。
|
||||
|
||||
## 包职责
|
||||
|
||||
| 包 | 职责 |
|
||||
|----|------|
|
||||
| `cmd/agent` | CLI 分发、配置加载、进程生命周期、HTTP server 启停 |
|
||||
| `internal/config` | 默认配置、JSON 读取、首次配置生成、环境变量覆盖、配置校验 |
|
||||
| `internal/status` | `/status` 与主动上报共用的 `HostStatus` JSON 数据模型 |
|
||||
| `internal/collector` | 平台相关主机、网络、资源、进程、TCP 监听采集 |
|
||||
| `internal/snapshot` | 子采集器聚合、partial 错误记录、并发安全缓存 |
|
||||
| `internal/httpapi` | `GET /health` 与 `GET /status`,查询参数 token 鉴权 |
|
||||
| `internal/reporter` | 周期性 POST 缓存快照,失败重试与日志记录 |
|
||||
| `internal/service` | Linux systemd 与 Windows SCM 服务生命周期封装 |
|
||||
|
||||
## 启动与生命周期
|
||||
|
||||
`agent` 无参数时等价于 `agent run`。
|
||||
|
||||
`agent run [-config config.json]` 流程:
|
||||
|
||||
1. 解析 `-config`,未指定时使用当前工作目录 `config.json`。
|
||||
2. 调用 `config.LoadOrCreate`。配置不存在时创建默认 JSON 并生成随机 `http.token`。
|
||||
3. 创建 `snapshot.Cache`,启动后台采集循环。
|
||||
4. 若 `report.enabled=true`,启动主动上报循环。
|
||||
5. 启动 HTTP server,默认监听 `0.0.0.0:8080`。
|
||||
6. 收到 `SIGINT` 或 `SIGTERM` 后优雅关闭 HTTP server。
|
||||
|
||||
服务命令 `install`、`uninstall`、`start`、`stop` 使用 `config.Load` 读取已有配置,不会自动创建配置文件。
|
||||
|
||||
## 配置加载流程
|
||||
|
||||
配置来源按以下顺序生效:
|
||||
|
||||
1. `config.Default()` 提供默认值。
|
||||
2. JSON 文件覆盖默认值。
|
||||
3. `MFD_AGENT_*` 环境变量覆盖 JSON。
|
||||
4. `validate` 校验安全和必填项。
|
||||
|
||||
关键规则:
|
||||
|
||||
- `http.token` 必填;`agent run` 首次生成配置时会自动写入随机 token。
|
||||
- `report.enabled=true` 时 `report.endpoint` 必填。
|
||||
- 自动生成的配置文件权限为 `0600`,因为其中包含 API token。
|
||||
- 非法布尔或时长环境变量会被忽略,保留文件中的原值。
|
||||
|
||||
## 采集与缓存流程
|
||||
|
||||
`snapshot.Cache.Run` 启动后立即采集一次,随后按 `collect.interval` 周期刷新。
|
||||
|
||||
每轮采集流程:
|
||||
|
||||
1. 用 `collect.timeout` 创建整体超时 context。
|
||||
2. 构造新的 `HostStatus`,默认 `agent.snapshot_status=ok`。
|
||||
3. 依次执行 `system`、`network`、`resource`、`process`、`socket` 采集器。
|
||||
4. 子采集器失败时记录到 `errors[]`,整份快照降级为 `partial`,其他字段继续保留。
|
||||
5. 用锁一次性发布最新快照,供 HTTP 与 reporter 并发读取。
|
||||
|
||||
`processes.status` 与 `tcp_listeners.status` 支持 `ok`、`partial`、`error`。例如 Linux 读取 `/proc/<pid>/fd` 权限不足时,TCP 端口仍会返回,但进程映射可能不完整并标记为 `partial`。
|
||||
|
||||
## HTTP 查询流程
|
||||
|
||||
所有 API 都要求查询参数 token:
|
||||
|
||||
```text
|
||||
GET /health?token=<http.token>
|
||||
GET /status?token=<http.token>
|
||||
```
|
||||
|
||||
处理流程:
|
||||
|
||||
1. `withQueryTokenAuth` 使用恒定时间比较校验 token。
|
||||
2. token 缺失、错误或配置 token 为空时返回 `401`。
|
||||
3. 非 `GET` 方法返回 `405`。
|
||||
4. `/health` 读取缓存元数据。首次快照未就绪时返回 `503` 和 `{"status":"starting"}`。
|
||||
5. `/status` 返回完整缓存 `HostStatus`。首次快照未就绪时返回 `503`。
|
||||
|
||||
HTTP 请求不会触发采集,避免高并发查询放大系统扫描成本。
|
||||
|
||||
## 主动上报流程
|
||||
|
||||
启用条件:
|
||||
|
||||
```json
|
||||
{
|
||||
"report": {
|
||||
"enabled": true,
|
||||
"endpoint": "http://center.example.internal/agent/status"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行流程:
|
||||
|
||||
1. `reporter.Run` 按 `report.interval` 定时执行。
|
||||
2. 从 `snapshot.Cache` 读取最新 `HostStatus`。
|
||||
3. 首次快照未就绪时跳过本轮。
|
||||
4. 以 `application/json` POST 到 `report.endpoint`。
|
||||
5. 非 2xx、网络错误或超时视为失败。
|
||||
6. 失败后等待短暂退避并重试一次。
|
||||
7. 最终失败只记录日志,不影响采集循环或本地 HTTP 服务。
|
||||
|
||||
上报载荷与 `/status` 响应结构完全一致。
|
||||
|
||||
## 服务生命周期流程
|
||||
|
||||
### Linux systemd
|
||||
|
||||
`agent install -config /path/config.json -exe /path/agent`:
|
||||
|
||||
1. 检查当前用户必须为 root。
|
||||
2. 生成 `/etc/systemd/system/<service.name>.service`。
|
||||
3. `ExecStart` 写入 `<exe> run -config <path>`。
|
||||
4. 执行 `systemctl daemon-reload`。
|
||||
5. 执行 `systemctl enable <service.name>`。
|
||||
|
||||
`start`、`stop` 调用 `systemctl start/stop`。`uninstall` 尽力 `disable --now`,删除 unit 文件后再次 `daemon-reload`。
|
||||
|
||||
### Windows SCM
|
||||
|
||||
Windows 通过 `sc.exe` 管理服务,不依赖 NSSM。
|
||||
|
||||
`install` 使用 `sc.exe create` 注册自动启动服务,`binPath` 为:
|
||||
|
||||
```text
|
||||
"<exe>" run -config "<config>"
|
||||
```
|
||||
|
||||
`start`、`stop`、`uninstall` 分别调用 `sc.exe start`、`sc.exe stop`、`sc.exe delete`。安装、删除和启停通常需要管理员权限。
|
||||
|
||||
## HostStatus 数据模型
|
||||
|
||||
`HostStatus` 是唯一对外状态模型,同时用于:
|
||||
|
||||
- `GET /status` 响应体
|
||||
- 主动上报 POST body
|
||||
- 测试中的快照断言
|
||||
|
||||
顶层字段包括:
|
||||
|
||||
- `agent`:版本、启动时间、快照时间、快照状态
|
||||
- `host`:机器标识、主机名、平台、架构、运行时长
|
||||
- `network`:网卡明细、IPv4/IPv6、MAC
|
||||
- `cpu`:逻辑核心、物理核心、使用率、负载、型号
|
||||
- `memory`:内存容量与使用率
|
||||
- `disks`:文件系统容量与使用率
|
||||
- `processes`:采集状态与完整进程列表
|
||||
- `tcp_listeners`:采集状态与 TCP 监听端口到进程映射
|
||||
- `errors`:子采集器失败原因
|
||||
|
||||
## 平台差异
|
||||
|
||||
Linux 采集主要读取 `/proc`、`/etc/os-release`、`net.Interfaces` 和 `statfs`。进程与 socket 映射受 `/proc/<pid>` 权限和进程生命周期竞争影响。
|
||||
|
||||
Windows 采集使用 Win32 API、Toolhelp、注册表、PSAPI、iphlpapi 和一次尽力 WMIC 命令。部分字段需要高权限读取进程句柄或令牌,失败时降级为空字段或 `partial`。
|
||||
|
||||
非 Linux/Windows 平台保留基础主机和网络采集,进程与 TCP 监听采集明确返回不支持并标记 `partial`。
|
||||
|
||||
369
docs/DEPLOYMENT.md
Normal file
369
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# MFD Monitor Agent 部署指南
|
||||
|
||||
本文档说明如何在 Linux 与 Windows 上编译、配置、安装并验收 MFD Monitor Agent。
|
||||
|
||||
## 概述
|
||||
|
||||
MFD Monitor Agent 是单二进制主机状态采集程序,具备以下能力:
|
||||
|
||||
- 后台周期采集主机状态并写入内存缓存
|
||||
- 本地 HTTP API:`GET /health?token=...`、`GET /status?token=...`(默认监听 `0.0.0.0:8080`,需配置 `http.token`)
|
||||
- 可选周期性向中心端 POST 与 `/status` 相同结构的 `HostStatus` JSON
|
||||
- CLI 管理服务生命周期:`run`、`install`、`uninstall`、`start`、`stop`、`version`
|
||||
|
||||
HTTP 接口与上报均读取同一份缓存,**不会在每次请求时实时扫描系统**。
|
||||
|
||||
更多内部流程见 [架构与工作流程](ARCHITECTURE.md),构建测试流程见 [开发流程](DEVELOPMENT.md)。
|
||||
|
||||
## 运行流程
|
||||
|
||||
`agent run` 的运行链路如下:
|
||||
|
||||
1. 解析 `-config`;未指定时使用当前目录 `config.json`。
|
||||
2. 配置不存在时自动生成默认配置与随机 `http.token`,文件权限为 `0600`。
|
||||
3. 加载 JSON,并应用 `MFD_AGENT_*` 环境变量覆盖。
|
||||
4. 启动 `snapshot.Cache.Run`,立即采集一次并按 `collect.interval` 周期刷新缓存。
|
||||
5. 如果 `report.enabled=true`,启动主动上报循环。
|
||||
6. 启动 HTTP API,默认监听 `0.0.0.0:8080`。
|
||||
7. 收到退出信号后优雅关闭 HTTP server。
|
||||
|
||||
单轮采集使用 `collect.timeout` 作为整体超时。子采集器失败不会导致整份 `/status` 失败,而是记录到 `errors[]` 并将 `agent.snapshot_status` 标记为 `partial`。
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| Go | 1.22 及以上(见 `go.mod`) |
|
||||
| Linux 服务化 | systemd |
|
||||
| 安装权限 | Linux `install` 需 **root**;Windows `install` 需**管理员** |
|
||||
| 运行账户 | 首版建议以高权限服务账户运行,以便采集完整进程列表与 TCP 监听→进程映射 |
|
||||
|
||||
> 非 Linux/Windows 平台可前台 `run`,但不支持 `install`/`start`/`stop` 服务命令。
|
||||
|
||||
## 编译与产物
|
||||
|
||||
### Linux / 交叉编译(bash)
|
||||
|
||||
```bash
|
||||
cd /path/to/mfd-monitor-agent
|
||||
./scripts/build-linux.sh
|
||||
```
|
||||
|
||||
默认输出:`dist/agent-linux-amd64`。
|
||||
|
||||
常用环境变量:
|
||||
|
||||
| 变量 | 含义 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `OUT_DIR` | 输出目录 | `dist/` |
|
||||
| `VERSION` | 写入二进制的版本号 | `0.1.0` |
|
||||
| `GOOS_TARGET` | 目标操作系统 | `linux` |
|
||||
| `GOARCH_TARGET` | 目标架构 | `amd64` |
|
||||
| `OUTPUT_NAME` | 输出文件名 | `agent-<GOOS>-<GOARCH>` |
|
||||
| `CGO_ENABLED` | 是否启用 CGO | `0` |
|
||||
|
||||
交叉编译 Windows 示例:
|
||||
|
||||
```bash
|
||||
GOOS_TARGET=windows GOARCH_TARGET=amd64 OUTPUT_NAME=agent-windows-amd64.exe ./scripts/build-linux.sh
|
||||
```
|
||||
|
||||
指定发布版本:
|
||||
|
||||
```bash
|
||||
VERSION=1.0.0 ./scripts/build-linux.sh
|
||||
./dist/agent-linux-amd64 version
|
||||
```
|
||||
|
||||
### Windows(PowerShell)
|
||||
|
||||
```powershell
|
||||
cd C:\path\to\mfd-monitor-agent
|
||||
.\scripts\build-windows.ps1
|
||||
```
|
||||
|
||||
默认输出:`dist\agent-windows-amd64.exe`。
|
||||
|
||||
构建产物目录 `dist/` 已在 `.gitignore` 中忽略,不会提交到仓库。
|
||||
|
||||
## 推荐目录布局
|
||||
|
||||
### Linux
|
||||
|
||||
```text
|
||||
/opt/mfd-monitor-agent/agent # 可执行文件
|
||||
/etc/mfd-monitor-agent/config.json # 配置文件
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```text
|
||||
C:\Program Files\MFD\agent\agent.exe
|
||||
C:\ProgramData\MFD\config.json
|
||||
```
|
||||
|
||||
仓库提供示例配置:[configs/example.json](../configs/example.json)。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### JSON 配置文件
|
||||
|
||||
`agent run` 在未指定 `-config` 时使用当前工作目录下的 `config.json`;若文件不存在,会自动生成含随机 `http.token` 的默认配置(权限 `0600`)。
|
||||
|
||||
`install` / `start` 等命令不会自动创建配置文件,需事先准备好 JSON。
|
||||
|
||||
```json
|
||||
{
|
||||
"host_id": "host-a",
|
||||
"http": {
|
||||
"listen_address": "0.0.0.0:8080",
|
||||
"token": "请替换为强随机值"
|
||||
},
|
||||
"collect": {
|
||||
"interval": "30s",
|
||||
"timeout": "10s"
|
||||
},
|
||||
"report": {
|
||||
"enabled": true,
|
||||
"endpoint": "http://center.example.internal/agent/status",
|
||||
"interval": "60s",
|
||||
"timeout": "5s"
|
||||
},
|
||||
"service": {
|
||||
"name": "mfd-monitor-agent",
|
||||
"display_name": "MFD Monitor Agent",
|
||||
"description": "采集主机状态并通过 HTTP 提供查询"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `host_id` | 机器标识;为空时采集器可回退为主机名 | `""` |
|
||||
| `http.listen_address` | HTTP 监听地址 | `0.0.0.0:8080` |
|
||||
| `http.token` | API 查询参数 `?token=` 鉴权密钥(必填) | 无(`run` 自动生成) |
|
||||
| `collect.interval` | 采集周期 | `30s` |
|
||||
| `collect.timeout` | 单次采集总超时 | `10s` |
|
||||
| `report.enabled` | 是否启用主动上报 | `false` |
|
||||
| `report.endpoint` | 中心端 POST URL | `""` |
|
||||
| `report.interval` | 上报周期 | `60s` |
|
||||
| `report.timeout` | 单次上报 HTTP 超时 | `5s` |
|
||||
| `service.name` | 服务名(systemd unit / Windows 服务名) | `mfd-monitor-agent` |
|
||||
| `service.display_name` | Windows 显示名称 | `MFD Monitor Agent` |
|
||||
| `service.description` | systemd unit 描述 | 见默认配置 |
|
||||
|
||||
**校验规则:**
|
||||
|
||||
- `http.token` 不能为空,否则 `config.Load` 失败。
|
||||
- `report.enabled=true` 时必须设置 `report.endpoint`,否则 `config.Load` 失败。
|
||||
|
||||
时长字段使用 Go duration 字符串,例如 `"30s"`、`"1m"`。
|
||||
|
||||
### 环境变量覆盖
|
||||
|
||||
在 JSON 加载后,以下环境变量可覆盖对应配置:
|
||||
|
||||
| 环境变量 | 配置字段 |
|
||||
|----------|----------|
|
||||
| `MFD_AGENT_HOST_ID` | `host_id` |
|
||||
| `MFD_AGENT_LISTEN` | `http.listen_address` |
|
||||
| `MFD_AGENT_TOKEN` | `http.token` |
|
||||
| `MFD_AGENT_COLLECT_INTERVAL` | `collect.interval` |
|
||||
| `MFD_AGENT_COLLECT_TIMEOUT` | `collect.timeout` |
|
||||
| `MFD_AGENT_REPORT_ENABLED` | `report.enabled` |
|
||||
| `MFD_AGENT_REPORT_ENDPOINT` | `report.endpoint` |
|
||||
| `MFD_AGENT_REPORT_INTERVAL` | `report.interval` |
|
||||
| `MFD_AGENT_REPORT_TIMEOUT` | `report.timeout` |
|
||||
|
||||
## CLI 命令
|
||||
|
||||
```bash
|
||||
agent run [-config /path/to/config.json]
|
||||
agent install [-config /path/to/config.json] [-exe /path/to/agent]
|
||||
agent uninstall [-config /path/to/config.json]
|
||||
agent start [-config /path/to/config.json]
|
||||
agent stop [-config /path/to/config.json]
|
||||
agent version
|
||||
```
|
||||
|
||||
无参数时默认执行 `run`。
|
||||
|
||||
## Linux systemd 部署
|
||||
|
||||
### 1. 安装文件
|
||||
|
||||
```bash
|
||||
sudo install -m 0755 dist/agent-linux-amd64 /opt/mfd-monitor-agent/agent
|
||||
sudo install -m 0644 configs/example.json /etc/mfd-monitor-agent/config.json
|
||||
# 按需编辑 host_id、report 等字段
|
||||
sudo ${EDITOR:-vi} /etc/mfd-monitor-agent/config.json
|
||||
```
|
||||
|
||||
### 2. 注册并启用服务
|
||||
|
||||
```bash
|
||||
sudo /opt/mfd-monitor-agent/agent install \
|
||||
-config /etc/mfd-monitor-agent/config.json \
|
||||
-exe /opt/mfd-monitor-agent/agent
|
||||
```
|
||||
|
||||
`install` 会:
|
||||
|
||||
1. 写入 `/etc/systemd/system/<service.name>.service`
|
||||
2. 执行 `systemctl daemon-reload`
|
||||
3. 执行 `systemctl enable <service.name>`
|
||||
|
||||
生成的 unit 中 `ExecStart` 形如:
|
||||
|
||||
```ini
|
||||
ExecStart=/opt/mfd-monitor-agent/agent run -config /etc/mfd-monitor-agent/config.json
|
||||
```
|
||||
|
||||
### 3. 启动与查看状态
|
||||
|
||||
```bash
|
||||
sudo /opt/mfd-monitor-agent/agent start -config /etc/mfd-monitor-agent/config.json
|
||||
# 或
|
||||
sudo systemctl start mfd-monitor-agent
|
||||
|
||||
sudo systemctl status mfd-monitor-agent
|
||||
sudo journalctl -u mfd-monitor-agent -f
|
||||
```
|
||||
|
||||
### 4. 停止与卸载
|
||||
|
||||
```bash
|
||||
sudo /opt/mfd-monitor-agent/agent stop -config /etc/mfd-monitor-agent/config.json
|
||||
sudo /opt/mfd-monitor-agent/agent uninstall -config /etc/mfd-monitor-agent/config.json
|
||||
```
|
||||
|
||||
`uninstall` 会 `disable --now` 服务、删除 unit 文件并 `daemon-reload`。
|
||||
|
||||
## Windows 服务部署
|
||||
|
||||
> 当前通过 `sc.exe` 注册 Windows 服务;`run` 为前台模式,**非** Go SCM 内嵌服务包装。服务启动后由 SCM 按 `binPath` 拉起 `agent run`。
|
||||
|
||||
### 1. 复制文件
|
||||
|
||||
将 `agent-windows-amd64.exe` 与 `config.json` 放到固定路径,例如:
|
||||
|
||||
- `C:\Program Files\MFD\agent\agent.exe`
|
||||
- `C:\ProgramData\MFD\config.json`
|
||||
|
||||
### 2. 注册服务(管理员 PowerShell)
|
||||
|
||||
```powershell
|
||||
cd "C:\Program Files\MFD\agent"
|
||||
.\agent.exe install -config "C:\ProgramData\MFD\config.json" -exe "C:\Program Files\MFD\agent\agent.exe"
|
||||
```
|
||||
|
||||
### 3. 启动与查询
|
||||
|
||||
```powershell
|
||||
.\agent.exe start -config "C:\ProgramData\MFD\config.json"
|
||||
sc.exe query mfd-monitor-agent
|
||||
```
|
||||
|
||||
也可在「服务」管理器中启停。
|
||||
|
||||
### 4. 卸载
|
||||
|
||||
```powershell
|
||||
.\agent.exe uninstall -config "C:\ProgramData\MFD\config.json"
|
||||
```
|
||||
|
||||
内部会先 `sc stop` 再 `sc delete`。
|
||||
|
||||
## 前台运行与验收
|
||||
|
||||
开发或临时排查时可前台运行:
|
||||
|
||||
```bash
|
||||
./agent run -config /etc/mfd-monitor-agent/config.json
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
将 `YOUR_TOKEN` 替换为配置文件中的 `http.token`:
|
||||
|
||||
```bash
|
||||
curl -s 'http://127.0.0.1:8080/health?token=YOUR_TOKEN' | jq .
|
||||
```
|
||||
|
||||
首次采集完成前可能返回 `503` 且 `{"status":"starting"}`。就绪后示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"snapshot_at": "2026-05-19T12:00:00Z",
|
||||
"snapshot_status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
`snapshot_status` 为 `partial` 表示部分子采集器失败,但接口仍返回 200。
|
||||
|
||||
### 完整状态
|
||||
|
||||
```bash
|
||||
curl -s 'http://127.0.0.1:8080/status?token=YOUR_TOKEN' | jq '.agent, .host, .processes.status, .tcp_listeners.status'
|
||||
```
|
||||
|
||||
浏览器调试示例:`http://<host>:8080/status?token=YOUR_TOKEN`
|
||||
|
||||
### 主动上报验收
|
||||
|
||||
1. 在配置中设置 `report.enabled=true` 与可达的 `report.endpoint`
|
||||
2. 在中心端确认收到 POST,Body 为完整 `HostStatus` JSON
|
||||
3. 上报失败仅记录日志并重试一次,**不会**导致 agent 进程退出或影响本地 HTTP
|
||||
|
||||
## 升级与回滚
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
sudo systemctl stop mfd-monitor-agent
|
||||
sudo install -m 0755 dist/agent-linux-amd64 /opt/mfd-monitor-agent/agent
|
||||
sudo systemctl start mfd-monitor-agent
|
||||
```
|
||||
|
||||
若 unit 中可执行路径未变,无需重新 `install`。
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
sc.exe stop mfd-monitor-agent
|
||||
# 替换 agent.exe
|
||||
sc.exe start mfd-monitor-agent
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- **默认监听所有网卡**(`0.0.0.0:8080`),`/health` 与 `/status` 均须通过查询参数 `?token=` 鉴权。
|
||||
- Token 会出现在 URL 中,代理与访问日志可能记录完整 URL;生产环境建议使用 HTTPS 反向代理、防火墙限制来源,并定期轮换 token。
|
||||
- 自动生成的 `config.json` 权限为 `0600`,请妥善保管其中的 `http.token`。
|
||||
- 服务账户权限越高,采集越完整;生产环境应在「采集完整性」与「最小权限」之间权衡。
|
||||
- `/status` 响应体可能较大(进程多、监听多时),注意中心端与本地内存占用。
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 处理建议 |
|
||||
|------|----------|----------|
|
||||
| 启动报错 `http.token is required` | 配置中 token 为空 | 在 JSON 中设置 `http.token`,或使用 `agent run` 自动生成 |
|
||||
| 启动报错 `report endpoint is required` | 开启上报但未配置 endpoint | 设置 `report.endpoint` 或关闭 `report.enabled` |
|
||||
| HTTP `401 unauthorized` | 缺少或错误的 `?token=` | 使用配置文件中的 `http.token` |
|
||||
| `install requires root` | Linux 非 root 执行 install | 使用 `sudo` |
|
||||
| `service install is not implemented` | 非 Linux/Windows | 仅使用 `run` 前台模式 |
|
||||
| `/health` 长期 `starting` | 首次采集未完成或采集卡住 | 检查 `collect.timeout`、系统负载与权限 |
|
||||
| `snapshot_status: partial` | 部分采集器失败 | 查看 `/status` 中 `errors` 数组 |
|
||||
| `processes.status: partial/error` | 权限不足或 /proc 部分不可读 | 以更高权限运行服务 |
|
||||
| `tcp_listeners.status: partial` | 端口→进程映射不完整 | Linux 检查 `/proc/<pid>/fd` 可读性;Windows 检查权限 |
|
||||
| 上报失败日志 | 网络、中心端 5xx、超时 | 检查 `report.endpoint` 与 `report.timeout`;失败会自动重试一次 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 项目简介:[README.md](../README.md)
|
||||
- 架构与流程:[docs/ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- 开发与构建:[docs/DEVELOPMENT.md](DEVELOPMENT.md)
|
||||
- 架构与规范:[AGENTS.md](../AGENTS.md)
|
||||
- 示例配置:[configs/example.json](../configs/example.json)
|
||||
127
docs/DEVELOPMENT.md
Normal file
127
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# MFD Monitor Agent 开发流程
|
||||
|
||||
本文档记录当前仓库的构建、测试和跨平台验证流程。
|
||||
|
||||
## 本地环境
|
||||
|
||||
- Go 版本见 [go.mod](../go.mod)
|
||||
- Linux 服务化验证需要 systemd
|
||||
- Windows 服务化验证需要管理员 PowerShell
|
||||
- 构建产物默认写入 `dist/`,该目录不提交到 Git
|
||||
|
||||
## 测试命令
|
||||
|
||||
运行全部单元测试:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
按包运行:
|
||||
|
||||
```bash
|
||||
go test ./internal/collector
|
||||
go test ./internal/httpapi
|
||||
go test ./internal/reporter
|
||||
go test ./internal/service
|
||||
go test ./internal/snapshot
|
||||
```
|
||||
|
||||
Windows 编译与测试验证:
|
||||
|
||||
```bash
|
||||
GOOS=windows GOARCH=amd64 go test ./...
|
||||
```
|
||||
|
||||
## Linux 构建
|
||||
|
||||
```bash
|
||||
./scripts/build-linux.sh
|
||||
```
|
||||
|
||||
默认产物:
|
||||
|
||||
```text
|
||||
dist/agent-linux-amd64
|
||||
```
|
||||
|
||||
常用变量:
|
||||
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `OUT_DIR` | 输出目录 | `dist` |
|
||||
| `VERSION` | 注入到 `cmd/agent.version` 的版本号 | `0.1.0` |
|
||||
| `GOOS_TARGET` | 目标系统 | `linux` |
|
||||
| `GOARCH_TARGET` | 目标架构 | `amd64` |
|
||||
| `OUTPUT_NAME` | 输出文件名 | `agent-<goos>-<goarch>` |
|
||||
| `CGO_ENABLED` | CGO 开关 | `0` |
|
||||
|
||||
版本号注入示例:
|
||||
|
||||
```bash
|
||||
VERSION=1.0.0 ./scripts/build-linux.sh
|
||||
./dist/agent-linux-amd64 version
|
||||
```
|
||||
|
||||
## Windows 构建
|
||||
|
||||
在 Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.\scripts\build-windows.ps1
|
||||
```
|
||||
|
||||
默认产物:
|
||||
|
||||
```text
|
||||
dist\agent-windows-amd64.exe
|
||||
```
|
||||
|
||||
从 Linux 交叉编译 Windows:
|
||||
|
||||
```bash
|
||||
GOOS_TARGET=windows GOARCH_TARGET=amd64 OUTPUT_NAME=agent-windows-amd64.exe ./scripts/build-linux.sh
|
||||
```
|
||||
|
||||
## 全平台构建
|
||||
|
||||
仓库包含 `scripts/build-all.sh`,用于一次构建 Linux amd64 与 Windows amd64 产物:
|
||||
|
||||
```bash
|
||||
./scripts/build-all.sh
|
||||
```
|
||||
|
||||
建议发布前执行:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
GOOS=windows GOARCH=amd64 go test ./...
|
||||
./scripts/build-all.sh
|
||||
```
|
||||
|
||||
## 开发验证重点
|
||||
|
||||
变更采集逻辑时,重点验证:
|
||||
|
||||
- 单个子采集器失败后 `agent.snapshot_status=partial`
|
||||
- `errors[]` 包含失败采集器名称和错误摘要
|
||||
- `processes.status` 与 `tcp_listeners.status` 正确反映 `ok`、`partial`、`error`
|
||||
- HTTP 请求只读取缓存,不触发实时采集
|
||||
- 上报载荷与 `/status` 的 `HostStatus` 结构一致
|
||||
- 上报失败不会影响本地 HTTP 服务
|
||||
|
||||
变更服务逻辑时,重点验证:
|
||||
|
||||
- Linux `install` 非 root 返回错误
|
||||
- systemd unit 的 `ExecStart` 保留 `run -config <path>`
|
||||
- Windows `sc.exe create` 的 `binPath` 包含 `run -config <path>`
|
||||
- `uninstall` 对已停止或不存在的服务尽力清理
|
||||
|
||||
## 文档关系
|
||||
|
||||
- [README.md](../README.md):项目入口与快速命令
|
||||
- [DEPLOYMENT.md](DEPLOYMENT.md):部署、验收、安全与排障
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md):运行时架构和工作流程
|
||||
- [AGENTS.md](../AGENTS.md):仓库规则与功能边界
|
||||
- [plan.md](../plan.md):历史设计草案,当前行为以代码和 docs 文档为准
|
||||
|
||||
@@ -7,23 +7,23 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// namedCollector binds a stable collector name to a collection function.
|
||||
// namedCollector 将稳定的采集器名称与采集函数绑定。
|
||||
type namedCollector struct {
|
||||
name string
|
||||
fn func(context.Context, *status.HostStatus) error
|
||||
}
|
||||
|
||||
// Name returns the collector name used in snapshot error records.
|
||||
// Name 返回用于快照错误记录的采集器名称。
|
||||
func (c namedCollector) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Collect runs the wrapped collection function.
|
||||
// Collect 执行绑定的采集函数。
|
||||
func (c namedCollector) Collect(ctx context.Context, out *status.HostStatus) error {
|
||||
return c.fn(ctx, out)
|
||||
}
|
||||
|
||||
// New returns the default collector set for the current platform.
|
||||
// New 返回当前平台的默认采集器集合。
|
||||
func New(hostID string) []snapshot.Collector {
|
||||
return []snapshot.Collector{
|
||||
namedCollector{name: "system", fn: func(ctx context.Context, out *status.HostStatus) error {
|
||||
|
||||
@@ -23,10 +23,13 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// socketInodesFunc 是测试可替换的 Linux socket inode 到进程映射函数。
|
||||
var socketInodesFunc = socketInodes
|
||||
|
||||
// readTCPListenersFunc 是测试可替换的 Linux TCP 表解析函数。
|
||||
var readTCPListenersFunc = readTCPListeners
|
||||
|
||||
// collectSystem fills Linux host identity, OS, architecture, and uptime fields.
|
||||
// collectSystem 从 Linux 填充主机标识、操作系统、架构与运行时长字段。
|
||||
func collectSystem(_ context.Context, hostID string, out *status.HostStatus) error {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
@@ -44,7 +47,7 @@ func collectSystem(_ context.Context, hostID string, out *status.HostStatus) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectNetwork fills Linux network interface, address, and MAC data.
|
||||
// collectNetwork 填充 Linux 网卡、地址与 MAC 数据。
|
||||
func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -75,7 +78,7 @@ func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectResource fills CPU, memory, and disk resource data from Linux proc files.
|
||||
// collectResource 从 Linux /proc 文件填充 CPU、内存与磁盘资源数据。
|
||||
func collectResource(ctx context.Context, out *status.HostStatus) error {
|
||||
out.CPU.LogicalCores = runtime.NumCPU()
|
||||
out.CPU.PhysicalCores, out.CPU.ModelName = readCPUInfo()
|
||||
@@ -90,7 +93,7 @@ func collectResource(ctx context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectProcesses reads /proc process directories and degrades to partial when some entries fail.
|
||||
// collectProcesses 读取 /proc 进程目录;部分条目失败时降级为 partial。
|
||||
func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
@@ -108,6 +111,7 @@ func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
}
|
||||
process, err := readProcess(pid)
|
||||
if err != nil {
|
||||
// 进程可能在遍历期间退出,或因权限限制无法读取;保留其他进程并降级为 partial。
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
@@ -124,12 +128,13 @@ func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectSockets reads Linux TCP listener tables and maps socket inodes to owning processes.
|
||||
// collectSockets 读取 Linux TCP 监听表,并将 socket inode 映射到所属进程。
|
||||
func collectSockets(_ context.Context, out *status.HostStatus) error {
|
||||
inodes, inodeErr := socketInodesFunc()
|
||||
var listeners []status.TCPListenerInfo
|
||||
var errs []error
|
||||
if inodeErr != nil {
|
||||
// inode 映射失败通常意味着端口仍可见但进程归属不完整,后续继续解析 TCP 表。
|
||||
errs = append(errs, inodeErr)
|
||||
}
|
||||
for _, file := range []struct {
|
||||
@@ -161,7 +166,7 @@ func collectSockets(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readOSRelease returns the Linux PRETTY_NAME value when /etc/os-release is readable.
|
||||
// readOSRelease 在可读时从 /etc/os-release 返回 PRETTY_NAME。
|
||||
func readOSRelease() string {
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
@@ -178,7 +183,7 @@ func readOSRelease() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// readUptime returns host uptime in seconds from /proc/uptime.
|
||||
// readUptime 从 /proc/uptime 返回主机运行时长(秒)。
|
||||
func readUptime() uint64 {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
@@ -192,7 +197,7 @@ func readUptime() uint64 {
|
||||
return uint64(value)
|
||||
}
|
||||
|
||||
// readCPUInfo returns physical CPU package count and the first model name from /proc/cpuinfo.
|
||||
// readCPUInfo 从 /proc/cpuinfo 返回物理 CPU 封装数与首个型号名称。
|
||||
func readCPUInfo() (int, string) {
|
||||
file, err := os.Open("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
@@ -222,7 +227,7 @@ func readCPUInfo() (int, string) {
|
||||
return len(physical), model
|
||||
}
|
||||
|
||||
// readLoadAvg returns the 1, 5, and 15 minute Linux load averages when available.
|
||||
// readLoadAvg 在可用时返回 Linux 1/5/15 分钟负载均值。
|
||||
func readLoadAvg() []float64 {
|
||||
data, err := os.ReadFile("/proc/loadavg")
|
||||
if err != nil {
|
||||
@@ -239,7 +244,7 @@ func readLoadAvg() []float64 {
|
||||
return values
|
||||
}
|
||||
|
||||
// readMemory returns Linux memory counters in bytes from /proc/meminfo.
|
||||
// readMemory 从 /proc/meminfo 返回以字节为单位的 Linux 内存计数。
|
||||
func readMemory() status.MemoryInfo {
|
||||
values := map[string]uint64{}
|
||||
file, err := os.Open("/proc/meminfo")
|
||||
@@ -254,8 +259,8 @@ func readMemory() status.MemoryInfo {
|
||||
continue
|
||||
}
|
||||
value, _ := strconv.ParseUint(fields[1], 10, 64)
|
||||
// /proc/meminfo reports memory in KiB; the API exposes bytes and
|
||||
// includes memory.unit so clients can render the values explicitly.
|
||||
// /proc/meminfo 以 KiB 报告内存;API 对外暴露字节,
|
||||
// 并通过 memory.unit 让客户端可明确展示单位。
|
||||
values[strings.TrimSuffix(fields[0], ":")] = value * 1024
|
||||
}
|
||||
total := values["MemTotal"]
|
||||
@@ -269,7 +274,7 @@ func readMemory() status.MemoryInfo {
|
||||
return status.MemoryInfo{Unit: "byte", Total: total, Used: used, Free: free, Available: available, UsedPercent: percent}
|
||||
}
|
||||
|
||||
// readDisks returns physical and network mounted filesystem usage from /proc/mounts and statfs.
|
||||
// readDisks 从 /proc/mounts 与 statfs 返回物理盘与网络挂载文件系统用量。
|
||||
func readDisks() ([]status.DiskInfo, error) {
|
||||
file, err := os.Open("/proc/mounts")
|
||||
if err != nil {
|
||||
@@ -285,6 +290,7 @@ func readDisks() ([]status.DiskInfo, error) {
|
||||
}
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(fields[1], &stat); err != nil {
|
||||
// 个别挂载点可能瞬时不可达;磁盘采集保持尽力返回其他挂载。
|
||||
continue
|
||||
}
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
@@ -305,7 +311,7 @@ func readDisks() ([]status.DiskInfo, error) {
|
||||
return disks, scanner.Err()
|
||||
}
|
||||
|
||||
// shouldIncludeDiskMount reports whether a mount represents a user-relevant disk.
|
||||
// shouldIncludeDiskMount 判断挂载点是否代表用户关心的磁盘。
|
||||
func shouldIncludeDiskMount(device string, fsType string, total uint64) bool {
|
||||
if total == 0 {
|
||||
return false
|
||||
@@ -319,7 +325,7 @@ func shouldIncludeDiskMount(device string, fsType string, total uint64) bool {
|
||||
return strings.HasPrefix(device, "/dev/")
|
||||
}
|
||||
|
||||
// readProcess returns one Linux process entry from /proc/<pid>.
|
||||
// readProcess 从 /proc/<pid> 返回一条 Linux 进程记录。
|
||||
func readProcess(pid int) (status.ProcessInfo, error) {
|
||||
base := filepath.Join("/proc", strconv.Itoa(pid))
|
||||
statusValues, err := readProcessStatus(filepath.Join(base, "status"))
|
||||
@@ -331,6 +337,7 @@ func readProcess(pid int) (status.ProcessInfo, error) {
|
||||
vms := parseKB(statusValues["VmSize"])
|
||||
username := ""
|
||||
if uid := firstField(statusValues["Uid"]); uid != "" {
|
||||
// 用户名解析失败不影响进程主体信息,保留空用户名表示未知。
|
||||
if u, err := user.LookupId(uid); err == nil {
|
||||
username = u.Username
|
||||
}
|
||||
@@ -339,6 +346,7 @@ func readProcess(pid int) (status.ProcessInfo, error) {
|
||||
if data, err := os.ReadFile(filepath.Join(base, "cmdline")); err == nil {
|
||||
cmdline = strings.Trim(strings.ReplaceAll(string(data), "\x00", " "), " ")
|
||||
}
|
||||
// exe 和 stat 都可能因权限或进程退出失败;这些字段按空值/零值降级。
|
||||
exe, _ := os.Readlink(filepath.Join(base, "exe"))
|
||||
statValues, _ := readProcessStat(filepath.Join(base, "stat"))
|
||||
return status.ProcessInfo{
|
||||
@@ -348,12 +356,12 @@ func readProcess(pid int) (status.ProcessInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// processStat contains process counters parsed from /proc/<pid>/stat.
|
||||
// processStat 包含从 /proc/<pid>/stat 解析的进程计数。
|
||||
type processStat struct {
|
||||
StartTime int64
|
||||
}
|
||||
|
||||
// readProcessStat parses Linux process stat counters from path.
|
||||
// readProcessStat 从路径解析 Linux 进程 stat 计数。
|
||||
func readProcessStat(path string) (processStat, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -366,7 +374,7 @@ func readProcessStat(path string) (processStat, error) {
|
||||
return processStat{StartTime: startTime}, nil
|
||||
}
|
||||
|
||||
// readProcessStatus parses key-value fields from /proc/<pid>/status.
|
||||
// readProcessStatus 从 /proc/<pid>/status 解析键值字段。
|
||||
func readProcessStatus(path string) (map[string]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -384,7 +392,7 @@ func readProcessStatus(path string) (map[string]string, error) {
|
||||
return values, scanner.Err()
|
||||
}
|
||||
|
||||
// socketInodes maps Linux socket inode numbers to best-effort owning process data.
|
||||
// socketInodes 将 Linux socket inode 映射到尽力获取的所属进程数据。
|
||||
func socketInodes() (map[string]status.ProcessInfo, error) {
|
||||
out := map[string]status.ProcessInfo{}
|
||||
var readErrors int
|
||||
@@ -400,6 +408,7 @@ func socketInodes() (map[string]status.ProcessInfo, error) {
|
||||
fdDir := filepath.Join(path, "fd")
|
||||
entries, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
// 无法读取 fd 目录会导致部分端口缺少 PID 映射,因此计数并向上报告 partial。
|
||||
readErrors++
|
||||
return nil
|
||||
}
|
||||
@@ -419,7 +428,7 @@ func socketInodes() (map[string]status.ProcessInfo, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// readTCPListeners parses a Linux TCP table and returns sockets in LISTEN state.
|
||||
// readTCPListeners 解析 Linux TCP 表并返回处于 LISTEN 状态的套接字。
|
||||
func readTCPListeners(path string, family string, inodes map[string]status.ProcessInfo) ([]status.TCPListenerInfo, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -429,7 +438,7 @@ func readTCPListeners(path string, family string, inodes map[string]status.Proce
|
||||
var items []status.TCPListenerInfo
|
||||
scanner := bufio.NewScanner(file)
|
||||
if scanner.Scan() {
|
||||
// Skip header.
|
||||
// 跳过表头行。
|
||||
}
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
@@ -449,7 +458,7 @@ func readTCPListeners(path string, family string, inodes map[string]status.Proce
|
||||
return items, scanner.Err()
|
||||
}
|
||||
|
||||
// readCPUUsagePercent samples /proc/stat over a short interval and returns best-effort usage.
|
||||
// readCPUUsagePercent 在短间隔内采样 /proc/stat 并返回尽力估算的 CPU 使用率。
|
||||
func readCPUUsagePercent(ctx context.Context) float64 {
|
||||
prevData, err := os.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
@@ -475,7 +484,7 @@ func readCPUUsagePercent(ctx context.Context) float64 {
|
||||
return cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal)
|
||||
}
|
||||
|
||||
// parseAddress decodes a Linux /proc/net/tcp address into IP and port values.
|
||||
// parseAddress 将 Linux /proc/net/tcp 地址解码为 IP 与端口。
|
||||
func parseAddress(raw string, family string) (string, int, error) {
|
||||
parts := strings.Split(raw, ":")
|
||||
if len(parts) != 2 {
|
||||
@@ -495,6 +504,7 @@ func parseAddress(raw string, family string) (string, int, error) {
|
||||
if family == "ipv6" && len(bytes) == 16 {
|
||||
reordered := make([]byte, 16)
|
||||
for i := 0; i < 16; i += 4 {
|
||||
// /proc/net/tcp6 以 32 位小端字组保存 IPv6 地址,需逐字反转为网络序。
|
||||
reordered[i], reordered[i+1], reordered[i+2], reordered[i+3] = bytes[i+3], bytes[i+2], bytes[i+1], bytes[i]
|
||||
}
|
||||
return net.IP(reordered).String(), int(port64), nil
|
||||
@@ -502,7 +512,7 @@ func parseAddress(raw string, family string) (string, int, error) {
|
||||
return "", 0, fmt.Errorf("invalid %s address", family)
|
||||
}
|
||||
|
||||
// parseProcessStatStartTime extracts field 22 from Linux /proc/<pid>/stat content.
|
||||
// parseProcessStatStartTime 从 Linux /proc/<pid>/stat 内容提取第 22 字段。
|
||||
func parseProcessStatStartTime(raw string) (int64, error) {
|
||||
end := strings.LastIndex(raw, ")")
|
||||
if end < 0 || end+2 >= len(raw) {
|
||||
@@ -515,7 +525,7 @@ func parseProcessStatStartTime(raw string) (int64, error) {
|
||||
return strconv.ParseInt(fields[19], 10, 64)
|
||||
}
|
||||
|
||||
// parseCPUTimes extracts idle and total jiffy counters from the first cpu line in /proc/stat.
|
||||
// parseCPUTimes 从 /proc/stat 首行 cpu 提取空闲与总 jiffies 计数。
|
||||
func parseCPUTimes(raw string) (idle uint64, total uint64, err error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(raw))
|
||||
for scanner.Scan() {
|
||||
@@ -544,7 +554,7 @@ func parseCPUTimes(raw string) (idle uint64, total uint64, err error) {
|
||||
return 0, 0, fmt.Errorf("cpu line not found")
|
||||
}
|
||||
|
||||
// cpuUsagePercent calculates non-idle CPU percentage between two /proc/stat samples.
|
||||
// cpuUsagePercent 根据两次 /proc/stat 采样计算非空闲 CPU 百分比。
|
||||
func cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal uint64) float64 {
|
||||
totalDelta := nextTotal - prevTotal
|
||||
if totalDelta == 0 {
|
||||
@@ -557,13 +567,13 @@ func cpuUsagePercent(prevIdle, prevTotal, nextIdle, nextTotal uint64) float64 {
|
||||
return float64(totalDelta-idleDelta) / float64(totalDelta) * 100
|
||||
}
|
||||
|
||||
// parseKB converts the first numeric kilobyte value in raw to bytes.
|
||||
// parseKB 将 raw 中首个以 KiB 表示的数值转换为字节。
|
||||
func parseKB(raw string) uint64 {
|
||||
value, _ := strconv.ParseUint(firstField(raw), 10, 64)
|
||||
return value * 1024
|
||||
}
|
||||
|
||||
// firstField returns the first whitespace-separated token in raw.
|
||||
// firstField 返回 raw 中首个以空白分隔的字段。
|
||||
func firstField(raw string) string {
|
||||
fields := strings.Fields(raw)
|
||||
if len(fields) == 0 {
|
||||
@@ -572,7 +582,7 @@ func firstField(raw string) string {
|
||||
return fields[0]
|
||||
}
|
||||
|
||||
// sortedKeys returns deterministic sorted keys from a string set.
|
||||
// sortedKeys 从字符串集合返回确定顺序的排序键列表。
|
||||
func sortedKeys(values map[string]struct{}) []string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// TestParseAddressIPv4 verifies Linux IPv4 socket addresses are decoded.
|
||||
// TestParseAddressIPv4 验证 Linux IPv4 套接字地址解码。
|
||||
func TestParseAddressIPv4(t *testing.T) {
|
||||
ip, port, err := parseAddress("0100007F:1F90", "ipv4")
|
||||
if err != nil {
|
||||
@@ -25,7 +25,7 @@ func TestParseAddressIPv4(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseAddressIPv6 verifies Linux IPv6 socket addresses are decoded.
|
||||
// TestParseAddressIPv6 验证 Linux IPv6 套接字地址解码。
|
||||
func TestParseAddressIPv6(t *testing.T) {
|
||||
ip, port, err := parseAddress("00000000000000000000000001000000:1F90", "ipv6")
|
||||
if err != nil {
|
||||
@@ -36,7 +36,7 @@ func TestParseAddressIPv6(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseProcessStatStartTime verifies field 22 parsing from /proc/<pid>/stat.
|
||||
// TestParseProcessStatStartTime 验证从 /proc/<pid>/stat 解析第 22 字段。
|
||||
func TestParseProcessStatStartTime(t *testing.T) {
|
||||
raw := "1234 (agent worker) S 1 2 3 4 5 0 0 0 0 0 11 22 0 0 20 0 1 0 987654 0 0"
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestParseProcessStatStartTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCPUUsageFromProcStatSamples verifies CPU usage from two /proc/stat samples.
|
||||
// TestCPUUsageFromProcStatSamples 验证由两次 /proc/stat 采样计算 CPU 使用率。
|
||||
func TestCPUUsageFromProcStatSamples(t *testing.T) {
|
||||
prevIdle, prevTotal, err := parseCPUTimes("cpu 100 0 100 800 0 0 0 0 0 0\n")
|
||||
if err != nil {
|
||||
@@ -66,7 +66,7 @@ func TestCPUUsageFromProcStatSamples(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadProcessStatReturnsStartTime verifies process start time is exposed.
|
||||
// TestReadProcessStatReturnsStartTime 验证进程启动时间被暴露。
|
||||
func TestReadProcessStatReturnsStartTime(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "stat")
|
||||
raw := "1234 (agent worker) S 1 2 3 4 5 0 0 0 0 0 11 22 0 0 20 0 1 0 987654 0 0"
|
||||
@@ -83,7 +83,7 @@ func TestReadProcessStatReturnsStartTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadMemoryReturnsBytesWithUnit verifies memory capacity fields are exposed in bytes.
|
||||
// TestReadMemoryReturnsBytesWithUnit 验证内存容量字段以字节暴露。
|
||||
func TestReadMemoryReturnsBytesWithUnit(t *testing.T) {
|
||||
meminfo, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
@@ -114,7 +114,7 @@ func TestReadMemoryReturnsBytesWithUnit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldIncludeDiskMount keeps physical and network disks while dropping pseudo mounts.
|
||||
// TestShouldIncludeDiskMount 保留物理盘与网络盘并排除伪挂载。
|
||||
func TestShouldIncludeDiskMount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -140,7 +140,7 @@ func TestShouldIncludeDiskMount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectSocketsMarksPartialWhenProcessFDsUnreadable verifies socket mapping degradation.
|
||||
// TestCollectSocketsMarksPartialWhenProcessFDsUnreadable 验证进程 fd 不可读时套接字映射降级。
|
||||
func TestCollectSocketsMarksPartialWhenProcessFDsUnreadable(t *testing.T) {
|
||||
oldSocketInodes := socketInodesFunc
|
||||
oldReadTCPListeners := readTCPListenersFunc
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// collectSystem fills portable host identity fields for non-Linux platforms.
|
||||
// collectSystem 为非 Linux/Windows 平台填充可移植的主机标识字段。
|
||||
func collectSystem(_ context.Context, hostID string, out *status.HostStatus) error {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
@@ -29,7 +29,7 @@ func collectSystem(_ context.Context, hostID string, out *status.HostStatus) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectNetwork fills interface, address, and MAC data using the portable net package.
|
||||
// collectNetwork 使用可移植的 net 包填充网卡、地址与 MAC 数据。
|
||||
func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -56,19 +56,19 @@ func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectResource fills portable resource fields available without platform-specific APIs.
|
||||
// collectResource 填充无需平台专用 API 的可移植资源字段。
|
||||
func collectResource(_ context.Context, out *status.HostStatus) error {
|
||||
out.CPU.LogicalCores = runtime.NumCPU()
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectProcesses reports partial process collection until platform-specific support is added.
|
||||
// collectProcesses 在尚未实现平台专用支持时,将进程采集标记为 partial。
|
||||
func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
out.Processes.Status = "partial"
|
||||
return fmt.Errorf("process collection is not implemented for %s without platform-specific support", runtime.GOOS)
|
||||
}
|
||||
|
||||
// collectSockets reports partial TCP listener collection until platform-specific support is added.
|
||||
// collectSockets 在尚未实现平台专用支持时,将 TCP 监听采集标记为 partial。
|
||||
func collectSockets(_ context.Context, out *status.HostStatus) error {
|
||||
out.TCPListeners.Status = "partial"
|
||||
return fmt.Errorf("tcp listener process mapping is not implemented for %s without platform-specific support", runtime.GOOS)
|
||||
|
||||
@@ -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")
|
||||
@@ -62,10 +64,14 @@ var (
|
||||
windowsLookupAccountSidWProc = windowsAdvapi32.NewProc("LookupAccountSidW")
|
||||
)
|
||||
|
||||
// windowsMemoryStatus mirrors the Windows MEMORYSTATUSEX structure used by GlobalMemoryStatusEx.
|
||||
// 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
|
||||
@@ -75,9 +81,11 @@ type windowsMemoryStatus struct {
|
||||
AvailExtendedVirtual uint64
|
||||
}
|
||||
|
||||
// windowsMIBTCPRowOwnerPID mirrors a MIB_TCPROW_OWNER_PID row returned by GetExtendedTcpTable.
|
||||
// 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
|
||||
@@ -85,9 +93,11 @@ type windowsMIBTCPRowOwnerPID struct {
|
||||
OwningPID uint32
|
||||
}
|
||||
|
||||
// windowsMIBTCP6RowOwnerPID mirrors a MIB_TCP6ROW_OWNER_PID row returned by GetExtendedTcpTable.
|
||||
// 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
|
||||
@@ -97,7 +107,8 @@ type windowsMIBTCP6RowOwnerPID struct {
|
||||
OwningPID uint32
|
||||
}
|
||||
|
||||
// windowsSystemInfo mirrors the subset of SYSTEM_INFO needed for CPU package sizing.
|
||||
// windowsSystemInfo 镜像 CPU 封装规模计算所需的 SYSTEM_INFO 子集。
|
||||
// 字段顺序必须匹配 Win32 ABI,当前仅使用处理器数量与掩码字段。
|
||||
type windowsSystemInfo struct {
|
||||
ProcessorArchitecture uint16
|
||||
Reserved uint16
|
||||
@@ -112,21 +123,23 @@ type windowsSystemInfo struct {
|
||||
ProcessorRevision uint16
|
||||
}
|
||||
|
||||
// windowsSystemTimes stores system CPU time counters as 100ns units.
|
||||
// windowsSystemTimes 以 100 纳秒为单位存储系统 CPU 时间计数。
|
||||
type windowsSystemTimes struct {
|
||||
Idle uint64
|
||||
Kernel uint64
|
||||
User uint64
|
||||
}
|
||||
|
||||
// windowsLogicalProcessorInformation mirrors SYSTEM_LOGICAL_PROCESSOR_INFORMATION for core counting.
|
||||
// windowsLogicalProcessorInformation 镜像用于统计核心的 SYSTEM_LOGICAL_PROCESSOR_INFORMATION。
|
||||
// Data 只作为占位保留 ABI 大小,当前仅读取 Relationship。
|
||||
type windowsLogicalProcessorInformation struct {
|
||||
ProcessorMask uintptr
|
||||
Relationship uint32
|
||||
Data [16]byte
|
||||
}
|
||||
|
||||
// windowsProcessMemoryCounters mirrors PROCESS_MEMORY_COUNTERS.
|
||||
// windowsProcessMemoryCounters 镜像 PROCESS_MEMORY_COUNTERS。
|
||||
// 字段宽度依赖平台指针大小,必须保持与 psapi 返回结构一致。
|
||||
type windowsProcessMemoryCounters struct {
|
||||
CB uint32
|
||||
PageFaultCount uint32
|
||||
@@ -140,19 +153,25 @@ type windowsProcessMemoryCounters struct {
|
||||
PeakPagefileUsage uintptr
|
||||
}
|
||||
|
||||
// windowsProcessDetails contains best-effort per-process fields requiring extra handles or commands.
|
||||
// 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 fills Windows host identity, OS, and architecture fields.
|
||||
// collectSystem 填充 Windows 主机标识、操作系统与架构字段。
|
||||
func collectSystem(_ context.Context, hostID string, out *status.HostStatus) error {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
@@ -170,7 +189,7 @@ func collectSystem(_ context.Context, hostID string, out *status.HostStatus) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectNetwork fills Windows interface, address, and MAC data using the portable net package.
|
||||
// collectNetwork 使用可移植 net 包填充 Windows 网卡、地址与 MAC 数据。
|
||||
func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -195,7 +214,7 @@ func collectNetwork(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectResource fills Windows CPU, memory, and disk resource data.
|
||||
// collectResource 填充 Windows CPU、内存与磁盘资源数据。
|
||||
func collectResource(ctx context.Context, out *status.HostStatus) error {
|
||||
var errs []error
|
||||
out.CPU.LogicalCores = runtime.NumCPU()
|
||||
@@ -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
|
||||
@@ -217,7 +238,7 @@ func collectResource(ctx context.Context, out *status.HostStatus) error {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// collectProcesses enumerates Windows processes with Toolhelp and returns best-effort names.
|
||||
// collectProcesses 通过 Toolhelp 枚举 Windows 进程并返回尽力获取的名称等信息。
|
||||
func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
processes, err := windowsProcessList()
|
||||
out.Processes.Items = processes
|
||||
@@ -229,11 +250,12 @@ func collectProcesses(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectSockets reads Windows TCP listener tables and maps listener PIDs to process names.
|
||||
// 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)
|
||||
@@ -259,7 +281,7 @@ func collectSockets(_ context.Context, out *status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// windowsReadMemory calls GlobalMemoryStatusEx and converts physical memory counters to API fields.
|
||||
// windowsReadMemory 调用 GlobalMemoryStatusEx 并将物理内存计数转换为 API 字段。
|
||||
func windowsReadMemory() (status.MemoryInfo, error) {
|
||||
memory := windowsMemoryStatus{Length: uint32(unsafe.Sizeof(windowsMemoryStatus{}))}
|
||||
ok, _, err := windowsGlobalMemoryStatusExProc.Call(uintptr(unsafe.Pointer(&memory)))
|
||||
@@ -269,7 +291,7 @@ func windowsReadMemory() (status.MemoryInfo, error) {
|
||||
return windowsMemoryInfoFromStatus(memory), nil
|
||||
}
|
||||
|
||||
// windowsVersion returns the legacy Windows major.minor.build tuple exposed by GetVersion.
|
||||
// windowsVersion 返回 GetVersion 暴露的传统 major.minor.build 版本元组。
|
||||
func windowsVersion() string {
|
||||
version, err := syscall.GetVersion()
|
||||
if err != nil {
|
||||
@@ -281,15 +303,16 @@ func windowsVersion() string {
|
||||
return fmt.Sprintf("%d.%d.%d", major, minor, build)
|
||||
}
|
||||
|
||||
// windowsUptimeSeconds returns host uptime in seconds from GetTickCount64.
|
||||
// windowsUptimeSeconds 从 GetTickCount64 返回主机运行时长(秒)。
|
||||
func windowsUptimeSeconds() uint64 {
|
||||
millis, _, _ := windowsGetTickCount64Proc.Call()
|
||||
return uint64(millis) / 1000
|
||||
}
|
||||
|
||||
// windowsPhysicalCores returns physical CPU core count from Windows processor topology.
|
||||
// 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 {
|
||||
@@ -319,7 +343,7 @@ func windowsPhysicalCores() int {
|
||||
return runtime.NumCPU()
|
||||
}
|
||||
|
||||
// windowsCPUModelName reads the first processor name from the registry.
|
||||
// windowsCPUModelName 从注册表读取首个处理器名称。
|
||||
func windowsCPUModelName() string {
|
||||
keyPath, _ := syscall.UTF16PtrFromString(`HARDWARE\DESCRIPTION\System\CentralProcessor\0`)
|
||||
var key syscall.Handle
|
||||
@@ -344,7 +368,7 @@ func windowsCPUModelName() string {
|
||||
return strings.TrimSpace(syscall.UTF16ToString(utf16s))
|
||||
}
|
||||
|
||||
// windowsReadCPUUsagePercent samples GetSystemTimes and returns best-effort CPU usage.
|
||||
// windowsReadCPUUsagePercent 采样 GetSystemTimes 并返回尽力估算的 CPU 使用率。
|
||||
func windowsReadCPUUsagePercent(ctx context.Context) float64 {
|
||||
prev, err := windowsReadSystemTimes()
|
||||
if err != nil {
|
||||
@@ -362,7 +386,7 @@ func windowsReadCPUUsagePercent(ctx context.Context) float64 {
|
||||
return windowsCPUUsagePercent(prev, next)
|
||||
}
|
||||
|
||||
// windowsReadSystemTimes returns idle, kernel, and user system counters from GetSystemTimes.
|
||||
// windowsReadSystemTimes 从 GetSystemTimes 返回空闲、内核与用户系统计数。
|
||||
func windowsReadSystemTimes() (windowsSystemTimes, error) {
|
||||
var idle, kernel, user syscall.Filetime
|
||||
ok, _, err := windowsGetSystemTimesProc.Call(
|
||||
@@ -376,7 +400,7 @@ func windowsReadSystemTimes() (windowsSystemTimes, error) {
|
||||
return windowsSystemTimes{Idle: windowsFiletimeUint64(idle), Kernel: windowsFiletimeUint64(kernel), User: windowsFiletimeUint64(user)}, nil
|
||||
}
|
||||
|
||||
// windowsCPUUsagePercent calculates non-idle CPU percentage between two Windows CPU samples.
|
||||
// windowsCPUUsagePercent 根据两次 Windows CPU 采样计算非空闲 CPU 百分比。
|
||||
func windowsCPUUsagePercent(prev windowsSystemTimes, next windowsSystemTimes) float64 {
|
||||
prevTotal := prev.Kernel + prev.User
|
||||
nextTotal := next.Kernel + next.User
|
||||
@@ -391,12 +415,12 @@ func windowsCPUUsagePercent(prev windowsSystemTimes, next windowsSystemTimes) fl
|
||||
return float64(totalDelta-idleDelta) / float64(totalDelta) * 100
|
||||
}
|
||||
|
||||
// windowsFiletimeUint64 converts a FILETIME to a monotonically comparable counter.
|
||||
// windowsFiletimeUint64 将 FILETIME 转换为可单调比较的计数。
|
||||
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.
|
||||
// windowsReadDisks 枚举 Windows 本地固定盘与远程盘并返回容量数据。
|
||||
func windowsReadDisks() ([]status.DiskInfo, error) {
|
||||
mask, _, _ := windowsGetLogicalDrivesProc.Call()
|
||||
if mask == 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
|
||||
}
|
||||
@@ -430,7 +456,7 @@ func windowsReadDisks() ([]status.DiskInfo, error) {
|
||||
return disks, errors.Join(errs...)
|
||||
}
|
||||
|
||||
// windowsDiskInfoFromSpace converts Windows disk capacity counters into API disk fields.
|
||||
// windowsDiskInfoFromSpace 将 Windows 磁盘容量计数转换为 API 磁盘字段。
|
||||
func windowsDiskInfoFromSpace(root string, driveType uint32, total uint64, free uint64) status.DiskInfo {
|
||||
used := uint64(0)
|
||||
if total >= free {
|
||||
@@ -446,7 +472,7 @@ func windowsDiskInfoFromSpace(root string, driveType uint32, total uint64, free
|
||||
}
|
||||
}
|
||||
|
||||
// windowsDriveTypeName maps Windows drive type constants to stable API labels.
|
||||
// windowsDriveTypeName 将 Windows 驱动器类型常量映射为稳定的 API 标签。
|
||||
func windowsDriveTypeName(driveType uint32) string {
|
||||
switch driveType {
|
||||
case windowsDriveFixed:
|
||||
@@ -458,7 +484,7 @@ func windowsDriveTypeName(driveType uint32) string {
|
||||
}
|
||||
}
|
||||
|
||||
// windowsMemoryInfoFromStatus converts Windows physical memory counters into HostStatus memory data.
|
||||
// windowsMemoryInfoFromStatus 将 Windows 物理内存计数转换为 HostStatus 内存数据。
|
||||
func windowsMemoryInfoFromStatus(memory windowsMemoryStatus) status.MemoryInfo {
|
||||
used := uint64(0)
|
||||
if memory.TotalPhys >= memory.AvailPhys {
|
||||
@@ -474,7 +500,7 @@ func windowsMemoryInfoFromStatus(memory windowsMemoryStatus) status.MemoryInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// windowsTCPListeners returns IPv4 or IPv6 TCP listeners from GetExtendedTcpTable.
|
||||
// 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 {
|
||||
@@ -490,9 +516,10 @@ func windowsTCPListeners(family int, familyName string, processes map[int]status
|
||||
return windowsParseTCP6Rows(table[4:], count, processes), nil
|
||||
}
|
||||
|
||||
// windowsExtendedTCPTable calls GetExtendedTcpTable and returns the raw table bytes.
|
||||
// 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)),
|
||||
@@ -522,13 +550,14 @@ func windowsExtendedTCPTable(family int) ([]byte, error) {
|
||||
return table, nil
|
||||
}
|
||||
|
||||
// windowsParseTCP4Rows converts raw MIB_TCPROW_OWNER_PID rows into listener records.
|
||||
// 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]))
|
||||
@@ -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(),
|
||||
@@ -546,13 +576,14 @@ func windowsParseTCP4Rows(rows []byte, count uint32, processes map[int]status.Pr
|
||||
return items
|
||||
}
|
||||
|
||||
// windowsParseTCP6Rows converts raw MIB_TCP6ROW_OWNER_PID rows into listener records.
|
||||
// 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]))
|
||||
@@ -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",
|
||||
@@ -568,13 +600,13 @@ func windowsParseTCP6Rows(rows []byte, count uint32, processes map[int]status.Pr
|
||||
return items
|
||||
}
|
||||
|
||||
// windowsTCPPortFromNetworkOrder decodes the low 16 bits Windows stores in network byte order.
|
||||
// windowsTCPPortFromNetworkOrder 解码 Windows 以网络字节序存储的低 16 位端口。
|
||||
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.
|
||||
// windowsProcessList 使用 Windows Toolhelp 快照 API 枚举进程。
|
||||
func windowsProcessList() ([]status.ProcessInfo, error) {
|
||||
cmdlines := windowsProcessCommandLines()
|
||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
@@ -604,7 +636,7 @@ func windowsProcessList() ([]status.ProcessInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// windowsProcessInfoFromEntry combines Toolhelp identity data with optional process details.
|
||||
// windowsProcessInfoFromEntry 合并 Toolhelp 身份数据与可选进程详情。
|
||||
func windowsProcessInfoFromEntry(entry syscall.ProcessEntry32, details windowsProcessDetails) status.ProcessInfo {
|
||||
name := syscall.UTF16ToString(entry.ExeFile[:])
|
||||
exe := details.Exe
|
||||
@@ -619,10 +651,11 @@ func windowsProcessInfoFromEntry(entry syscall.ProcessEntry32, details windowsPr
|
||||
}
|
||||
}
|
||||
|
||||
// windowsReadProcessDetails opens a process and reads fields that may require elevated privileges.
|
||||
// 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)
|
||||
@@ -634,7 +667,7 @@ func windowsReadProcessDetails(pid uint32) windowsProcessDetails {
|
||||
return details
|
||||
}
|
||||
|
||||
// windowsQueryProcessImageName returns the full process image path when access allows it.
|
||||
// windowsQueryProcessImageName 在访问允许时返回完整进程映像路径。
|
||||
func windowsQueryProcessImageName(handle syscall.Handle) string {
|
||||
buf := make([]uint16, windowsMaxPath)
|
||||
size := uint32(len(buf))
|
||||
@@ -650,7 +683,7 @@ func windowsQueryProcessImageName(handle syscall.Handle) string {
|
||||
return syscall.UTF16ToString(buf[:size])
|
||||
}
|
||||
|
||||
// windowsQueryProcessTimes returns start time and lifetime-average CPU percentage.
|
||||
// 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 {
|
||||
@@ -666,7 +699,7 @@ func windowsQueryProcessTimes(handle syscall.Handle) (int64, float64) {
|
||||
return startNanos / int64(time.Second), percent
|
||||
}
|
||||
|
||||
// windowsQueryProcessMemory returns working set and pagefile usage when access allows it.
|
||||
// windowsQueryProcessMemory 在访问允许时返回工作集与页面文件占用。
|
||||
func windowsQueryProcessMemory(handle syscall.Handle) (uint64, uint64) {
|
||||
counters := windowsProcessMemoryCounters{CB: uint32(unsafe.Sizeof(windowsProcessMemoryCounters{}))}
|
||||
ok, _, _ := windowsGetProcessMemoryInfoProc.Call(
|
||||
@@ -680,7 +713,7 @@ func windowsQueryProcessMemory(handle syscall.Handle) (uint64, uint64) {
|
||||
return uint64(counters.WorkingSetSize), uint64(counters.PagefileUsage)
|
||||
}
|
||||
|
||||
// windowsQueryProcessUsername returns DOMAIN\user for a process token when access allows it.
|
||||
// windowsQueryProcessUsername 在访问允许时返回进程令牌的 DOMAIN\user。
|
||||
func windowsQueryProcessUsername(handle syscall.Handle) string {
|
||||
var token syscall.Handle
|
||||
ok, _, _ := windowsOpenProcessTokenProc.Call(uintptr(handle), uintptr(windowsTokenQuery), uintptr(unsafe.Pointer(&token)))
|
||||
@@ -702,7 +735,7 @@ func windowsQueryProcessUsername(handle syscall.Handle) string {
|
||||
return windowsLookupAccountName(sid)
|
||||
}
|
||||
|
||||
// windowsLookupAccountName resolves a SID pointer to DOMAIN\name.
|
||||
// windowsLookupAccountName 将 SID 指针解析为 DOMAIN\name。
|
||||
func windowsLookupAccountName(sid uintptr) string {
|
||||
var nameSize, domainSize uint32
|
||||
var sidType uint32
|
||||
@@ -732,13 +765,14 @@ func windowsLookupAccountName(sid uintptr) string {
|
||||
return domainName + `\` + account
|
||||
}
|
||||
|
||||
// windowsProcessCommandLines reads command lines in one best-effort WMIC call to avoid per-process shelling.
|
||||
// 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))
|
||||
@@ -760,7 +794,7 @@ func windowsProcessCommandLines() map[int]string {
|
||||
return out
|
||||
}
|
||||
|
||||
// windowsProcessMap returns process metadata keyed by PID for TCP owner mapping.
|
||||
// windowsProcessMap 返回以 PID 为键的进程元数据,供 TCP 属主映射使用。
|
||||
func windowsProcessMap() (map[int]status.ProcessInfo, error) {
|
||||
processes, err := windowsProcessList()
|
||||
out := make(map[int]status.ProcessInfo, len(processes))
|
||||
@@ -770,7 +804,7 @@ func windowsProcessMap() (map[int]status.ProcessInfo, error) {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// windowsSortedKeys returns deterministic sorted keys from a string set.
|
||||
// windowsSortedKeys 从字符串集合返回确定顺序的排序键列表。
|
||||
func windowsSortedKeys(values map[string]struct{}) []string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWindowsMemoryInfoFromStatus verifies Windows memory counters are exposed in bytes.
|
||||
// TestWindowsMemoryInfoFromStatus 验证 Windows 内存计数以字节对外暴露。
|
||||
func TestWindowsMemoryInfoFromStatus(t *testing.T) {
|
||||
got := windowsMemoryInfoFromStatus(windowsMemoryStatus{
|
||||
TotalPhys: 10,
|
||||
@@ -25,14 +25,14 @@ func TestWindowsMemoryInfoFromStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsTCPPortFromNetworkOrder verifies Windows TCP port values are decoded.
|
||||
// TestWindowsTCPPortFromNetworkOrder 验证 Windows TCP 端口值被正确解码。
|
||||
func TestWindowsTCPPortFromNetworkOrder(t *testing.T) {
|
||||
if got := windowsTCPPortFromNetworkOrder(0x0000901f); got != 8080 {
|
||||
t.Fatalf("port = %d, want 8080", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsDiskInfoFromSpace verifies Windows disk counters are mapped to API disk fields.
|
||||
// TestWindowsDiskInfoFromSpace 验证 Windows 磁盘计数映射到 API 磁盘字段。
|
||||
func TestWindowsDiskInfoFromSpace(t *testing.T) {
|
||||
got := windowsDiskInfoFromSpace(`C:\`, windowsDriveFixed, 100, 40)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestWindowsDiskInfoFromSpace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsCPUUsagePercent verifies idle and kernel counters are converted to busy percentage.
|
||||
// TestWindowsCPUUsagePercent 验证空闲与内核计数转换为繁忙百分比。
|
||||
func TestWindowsCPUUsagePercent(t *testing.T) {
|
||||
prev := windowsSystemTimes{Idle: 100, Kernel: 500, User: 500}
|
||||
next := windowsSystemTimes{Idle: 150, Kernel: 650, User: 700}
|
||||
@@ -62,7 +62,7 @@ func TestWindowsCPUUsagePercent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsProcessInfoFromEntry verifies Toolhelp process data is enriched with memory and command line data.
|
||||
// TestWindowsProcessInfoFromEntry 验证 Toolhelp 进程数据补充内存与命令行信息。
|
||||
func TestWindowsProcessInfoFromEntry(t *testing.T) {
|
||||
entry := syscall.ProcessEntry32{ProcessID: 123, ParentProcessID: 12}
|
||||
copy(entry.ExeFile[:], syscall.StringToUTF16("agent.exe"))
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// networkAddressSets separates interface addresses into deterministic IPv4 and IPv6 lists.
|
||||
// networkAddressSets 将接口地址拆分为确定顺序的 IPv4 与 IPv6 列表。
|
||||
func networkAddressSets(values map[string]struct{}) (ipv4s []string, ipv6s []string) {
|
||||
for value := range values {
|
||||
host, _, err := net.ParseCIDR(value)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNetworkAddressSetsSeparatesIPv4AndIPv6 verifies flattened IP lists are split by address family.
|
||||
// TestNetworkAddressSetsSeparatesIPv4AndIPv6 验证展平 IP 列表按地址族拆分。
|
||||
func TestNetworkAddressSetsSeparatesIPv4AndIPv6(t *testing.T) {
|
||||
ipv4s, ipv6s := networkAddressSets(map[string]struct{}{
|
||||
"fe80::1/64": {},
|
||||
|
||||
@@ -1,62 +1,85 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config contains all runtime, reporting, collection, and service settings.
|
||||
// DefaultConfigFileName 是未指定 -config 时在工作目录下使用的默认配置文件名。
|
||||
const DefaultConfigFileName = "config.json"
|
||||
|
||||
// Config 包含运行时、上报、采集与服务注册相关的全部配置项。
|
||||
type Config struct {
|
||||
HostID string `json:"host_id"`
|
||||
HTTP HTTPConfig `json:"http"`
|
||||
// HostID 是对外上报和查询时使用的机器标识;为空时采集器会尽力回退为主机名。
|
||||
HostID string `json:"host_id"`
|
||||
// HTTP 控制本地查询 API 的监听与 token 鉴权。
|
||||
HTTP HTTPConfig `json:"http"`
|
||||
// Collect 控制后台快照采集周期与单轮超时。
|
||||
Collect CollectConfig `json:"collect"`
|
||||
Report ReportConfig `json:"report"`
|
||||
// Report 控制可选的中心端主动上报流程。
|
||||
Report ReportConfig `json:"report"`
|
||||
// Service 控制 Linux systemd unit 与 Windows 服务注册元数据。
|
||||
Service ServiceConfig `json:"service"`
|
||||
}
|
||||
|
||||
// HTTPConfig controls the local HTTP API listener.
|
||||
// HTTPConfig 控制本地 HTTP API 的监听地址与查询参数鉴权 token。
|
||||
type HTTPConfig struct {
|
||||
// ListenAddress 是 HTTP API 监听地址,默认 0.0.0.0:8080 会暴露到所有网卡。
|
||||
ListenAddress string `json:"listen_address"`
|
||||
// Token 用于 GET 请求查询参数 ?token= 的鉴权,禁止为空。
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// CollectConfig controls periodic host status collection.
|
||||
// CollectConfig 控制周期性主机状态采集。
|
||||
type CollectConfig struct {
|
||||
// Interval 是后台刷新 HostStatus 缓存的周期。
|
||||
Interval Duration `json:"interval"`
|
||||
Timeout Duration `json:"timeout"`
|
||||
// Timeout 是单轮聚合采集的整体截止时间。
|
||||
Timeout Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
// ReportConfig controls optional periodic status POST reporting.
|
||||
// ReportConfig 控制可选的周期性状态 POST 上报。
|
||||
type ReportConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
// Enabled 控制是否启动主动上报循环。
|
||||
Enabled bool `json:"enabled"`
|
||||
// Endpoint 是中心端接收 HostStatus JSON 的 HTTP URL,启用上报时必填。
|
||||
Endpoint string `json:"endpoint"`
|
||||
// Interval 是两次上报尝试之间的周期。
|
||||
Interval Duration `json:"interval"`
|
||||
Timeout Duration `json:"timeout"`
|
||||
// Timeout 是单次 POST 请求的 HTTP 客户端超时。
|
||||
Timeout Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
// ServiceConfig controls operating system service registration metadata.
|
||||
// ServiceConfig 控制操作系统服务注册时使用的元数据。
|
||||
type ServiceConfig struct {
|
||||
Name string `json:"name"`
|
||||
// Name 是 systemd unit 名称和 Windows 服务名。
|
||||
Name string `json:"name"`
|
||||
// DisplayName 是 Windows 服务管理器中显示的名称。
|
||||
DisplayName string `json:"display_name"`
|
||||
// Description 是 systemd unit 与 Windows 服务的说明文本。
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Duration wraps time.Duration so JSON config can use Go duration strings.
|
||||
// Duration 封装 time.Duration,使 JSON 配置可使用 Go 时长字符串(如 "30s")。
|
||||
type Duration time.Duration
|
||||
|
||||
// Duration returns the standard library duration value.
|
||||
// Duration 返回标准库的 time.Duration 值。
|
||||
func (d Duration) Duration() time.Duration {
|
||||
return time.Duration(d)
|
||||
}
|
||||
|
||||
// MarshalJSON writes the duration as a Go duration string such as "30s".
|
||||
// MarshalJSON 将时长序列化为 Go 时长字符串,例如 "30s"。
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a Go duration string from JSON.
|
||||
// UnmarshalJSON 从 JSON 解析 Go 时长字符串。
|
||||
func (d *Duration) UnmarshalJSON(data []byte) error {
|
||||
var raw string
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
@@ -70,10 +93,10 @@ func (d *Duration) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default returns safe local defaults for running the agent.
|
||||
// Default 返回内存中的默认配置(不含 token,需由配置文件或 LoadOrCreate 提供)。
|
||||
func Default() Config {
|
||||
return Config{
|
||||
HTTP: HTTPConfig{ListenAddress: "127.0.0.1:8080"},
|
||||
HTTP: HTTPConfig{ListenAddress: "0.0.0.0:8080"},
|
||||
Collect: CollectConfig{
|
||||
Interval: Duration(30 * time.Second),
|
||||
Timeout: Duration(10 * time.Second),
|
||||
@@ -91,26 +114,87 @@ func Default() Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Load reads optional JSON config, applies environment overrides, and validates required settings.
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Default()
|
||||
if path != "" {
|
||||
data, err := os.ReadFile(path)
|
||||
// ResolvePath 将 CLI 传入的配置路径解析为实际文件路径。
|
||||
func ResolvePath(explicit string) string {
|
||||
if explicit != "" {
|
||||
return explicit
|
||||
}
|
||||
return DefaultConfigFileName
|
||||
}
|
||||
|
||||
// LoadOrCreate 在配置文件不存在时写入含随机 token 的默认配置,再加载。
|
||||
// 仅供 agent run 使用;服务安装等场景应使用 Load。
|
||||
func LoadOrCreate(path string) (Config, error) {
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
cfg := Default()
|
||||
// 自动生成配置时必须创建强随机 token,避免默认空 token 暴露主机状态 API。
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
return cfg, fmt.Errorf("generate http token: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
cfg.HTTP.Token = token
|
||||
if err := writeConfigFile(path, cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
}
|
||||
return Load(path)
|
||||
}
|
||||
|
||||
// Load 读取 JSON 配置文件,应用环境变量覆盖,并校验必填项。
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Default()
|
||||
if path == "" {
|
||||
return cfg, errors.New("config path is required")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
applyEnv(&cfg)
|
||||
if cfg.Report.Enabled && cfg.Report.Endpoint == "" {
|
||||
return cfg, errors.New("report endpoint is required when reporting is enabled")
|
||||
if err := validate(&cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// applyEnv overlays supported MFD_AGENT_* environment variables onto cfg.
|
||||
// validate 检查配置中的必填与安全约束。
|
||||
func validate(cfg *Config) error {
|
||||
if cfg.HTTP.Token == "" {
|
||||
return errors.New("http.token is required")
|
||||
}
|
||||
if cfg.Report.Enabled && cfg.Report.Endpoint == "" {
|
||||
return errors.New("report endpoint is required when reporting is enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeConfigFile 将配置以缩进 JSON 写入 path,权限 0600。
|
||||
func writeConfigFile(path string, cfg Config) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal config: %w", err)
|
||||
}
|
||||
data = append(data, '\n')
|
||||
// 配置内包含 HTTP token,默认只允许当前用户读写。
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
return fmt.Errorf("write config %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateToken 生成用于 HTTP 查询参数鉴权的随机十六进制字符串。
|
||||
func generateToken() (string, error) {
|
||||
buf := make([]byte, 32)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
// applyEnv 将支持的 MFD_AGENT_* 环境变量覆盖到 cfg 上。
|
||||
func applyEnv(cfg *Config) {
|
||||
if value := os.Getenv("MFD_AGENT_HOST_ID"); value != "" {
|
||||
cfg.HostID = value
|
||||
@@ -118,7 +202,11 @@ func applyEnv(cfg *Config) {
|
||||
if value := os.Getenv("MFD_AGENT_LISTEN"); value != "" {
|
||||
cfg.HTTP.ListenAddress = value
|
||||
}
|
||||
if value := os.Getenv("MFD_AGENT_TOKEN"); value != "" {
|
||||
cfg.HTTP.Token = value
|
||||
}
|
||||
if value := os.Getenv("MFD_AGENT_REPORT_ENABLED"); value != "" {
|
||||
// 非法环境变量值保持文件配置不变,避免拼写错误让 agent 无法启动。
|
||||
if parsed, err := strconv.ParseBool(value); err == nil {
|
||||
cfg.Report.Enabled = parsed
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestDefaultConfigUsesLocalhostAndSaneIntervals verifies safe local defaults.
|
||||
func TestDefaultConfigUsesLocalhostAndSaneIntervals(t *testing.T) {
|
||||
// TestDefaultConfigUsesAllInterfacesAndSaneIntervals 验证默认配置监听所有网卡且采集间隔合理。
|
||||
func TestDefaultConfigUsesAllInterfacesAndSaneIntervals(t *testing.T) {
|
||||
cfg := Default()
|
||||
|
||||
if cfg.HTTP.ListenAddress != "127.0.0.1:8080" {
|
||||
if cfg.HTTP.ListenAddress != "0.0.0.0:8080" {
|
||||
t.Fatalf("listen address = %q", cfg.HTTP.ListenAddress)
|
||||
}
|
||||
if cfg.HTTP.Token != "" {
|
||||
t.Fatalf("default config should not include token")
|
||||
}
|
||||
if cfg.Collect.Interval <= 0 || cfg.Collect.Timeout <= 0 {
|
||||
t.Fatalf("collect intervals must be positive: %+v", cfg.Collect)
|
||||
}
|
||||
@@ -21,15 +26,85 @@ func TestDefaultConfigUsesLocalhostAndSaneIntervals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadAppliesEnvironmentOverrides verifies environment variables override defaults.
|
||||
// TestResolvePathUsesDefaultFileName 验证空路径解析为 config.json。
|
||||
func TestResolvePathUsesDefaultFileName(t *testing.T) {
|
||||
if got := ResolvePath(""); got != DefaultConfigFileName {
|
||||
t.Fatalf("path = %q", got)
|
||||
}
|
||||
if got := ResolvePath("/etc/agent.json"); got != "/etc/agent.json" {
|
||||
t.Fatalf("path = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadOrCreateWritesConfigWithToken 验证缺失配置文件时会生成含 token 的 JSON。
|
||||
func TestLoadOrCreateWritesConfigWithToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
|
||||
cfg, err := LoadOrCreate(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.HTTP.Token == "" {
|
||||
t.Fatal("expected generated token")
|
||||
}
|
||||
if cfg.HTTP.ListenAddress != "0.0.0.0:8080" {
|
||||
t.Fatalf("listen address = %q", cfg.HTTP.ListenAddress)
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("file mode = %o", info.Mode().Perm())
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var onDisk Config
|
||||
if err := json.Unmarshal(data, &onDisk); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if onDisk.HTTP.Token != cfg.HTTP.Token {
|
||||
t.Fatalf("token mismatch: disk=%q loaded=%q", onDisk.HTTP.Token, cfg.HTTP.Token)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRejectsEmptyToken 验证未配置 token 时无法启动。
|
||||
func TestLoadRejectsEmptyToken(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = file.WriteString(`{"http":{"listen_address":"0.0.0.0:8080","token":""}}`)
|
||||
_ = file.Close()
|
||||
|
||||
_, err = Load(file.Name())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty token")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadAppliesEnvironmentOverrides 验证环境变量可覆盖默认配置。
|
||||
func TestLoadAppliesEnvironmentOverrides(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = file.WriteString(`{"http":{"listen_address":"0.0.0.0:8080","token":"base-token"}}`)
|
||||
_ = file.Close()
|
||||
|
||||
t.Setenv("MFD_AGENT_LISTEN", "127.0.0.1:9090")
|
||||
t.Setenv("MFD_AGENT_TOKEN", "env-token")
|
||||
t.Setenv("MFD_AGENT_REPORT_ENABLED", "true")
|
||||
t.Setenv("MFD_AGENT_REPORT_ENDPOINT", "http://center/status")
|
||||
t.Setenv("MFD_AGENT_REPORT_INTERVAL", "2s")
|
||||
t.Setenv("MFD_AGENT_HOST_ID", "host-a")
|
||||
|
||||
cfg, err := Load("")
|
||||
cfg, err := Load(file.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -37,6 +112,9 @@ func TestLoadAppliesEnvironmentOverrides(t *testing.T) {
|
||||
if cfg.HTTP.ListenAddress != "127.0.0.1:9090" {
|
||||
t.Fatalf("listen address = %q", cfg.HTTP.ListenAddress)
|
||||
}
|
||||
if cfg.HTTP.Token != "env-token" {
|
||||
t.Fatalf("token = %q", cfg.HTTP.Token)
|
||||
}
|
||||
if !cfg.Report.Enabled || cfg.Report.Endpoint != "http://center/status" {
|
||||
t.Fatalf("report config not applied: %+v", cfg.Report)
|
||||
}
|
||||
@@ -48,13 +126,13 @@ func TestLoadAppliesEnvironmentOverrides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadJSONConfigFile verifies JSON config files are parsed into Config.
|
||||
// TestLoadJSONConfigFile 验证可从 JSON 配置文件解析 Config。
|
||||
func TestLoadJSONConfigFile(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = file.WriteString(`{"http":{"listen_address":"127.0.0.1:7070"},"collect":{"interval":"3s","timeout":"1s"}}`)
|
||||
_, _ = file.WriteString(`{"http":{"listen_address":"127.0.0.1:7070","token":"secret"},"collect":{"interval":"3s","timeout":"1s"}}`)
|
||||
_ = file.Close()
|
||||
|
||||
cfg, err := Load(file.Name())
|
||||
@@ -65,6 +143,9 @@ func TestLoadJSONConfigFile(t *testing.T) {
|
||||
if cfg.HTTP.ListenAddress != "127.0.0.1:7070" {
|
||||
t.Fatalf("listen address = %q", cfg.HTTP.ListenAddress)
|
||||
}
|
||||
if cfg.HTTP.Token != "secret" {
|
||||
t.Fatalf("token = %q", cfg.HTTP.Token)
|
||||
}
|
||||
if cfg.Collect.Interval.Duration() != 3*time.Second || cfg.Collect.Timeout.Duration() != time.Second {
|
||||
t.Fatalf("collect config = %+v", cfg.Collect)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// Provider supplies the latest cached HostStatus to HTTP handlers.
|
||||
// Provider 向 HTTP 处理器提供最新缓存的 HostStatus。
|
||||
type Provider interface {
|
||||
// Get returns the latest snapshot and whether a snapshot is ready.
|
||||
// Get 返回最新快照及快照是否已就绪。
|
||||
Get() (status.HostStatus, bool)
|
||||
}
|
||||
|
||||
// StaticProvider is a test provider with a fixed snapshot response.
|
||||
// StaticProvider 是用于测试的固定快照提供者。
|
||||
type StaticProvider struct {
|
||||
Snapshot status.HostStatus
|
||||
OK bool
|
||||
}
|
||||
|
||||
// Get returns the configured static snapshot and readiness flag.
|
||||
// Get 返回配置的静态快照与就绪标志。
|
||||
func (p StaticProvider) Get() (status.HostStatus, bool) {
|
||||
return p.Snapshot, p.OK
|
||||
}
|
||||
|
||||
// New creates the HTTP API handler for health and status endpoints.
|
||||
func New(provider Provider) http.Handler {
|
||||
// New 创建提供 health 与 status 端点的 HTTP API 处理器;token 用于查询参数 ?token= 鉴权。
|
||||
// 两个端点都只读取 provider 缓存,不会在请求路径上触发实时系统扫描。
|
||||
func New(provider Provider, token string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
@@ -58,10 +60,32 @@ func New(provider Provider) http.Handler {
|
||||
}
|
||||
writeJSON(w, http.StatusOK, snapshot)
|
||||
})
|
||||
return mux
|
||||
return withQueryTokenAuth(token, mux)
|
||||
}
|
||||
|
||||
// writeJSON serializes value as JSON with a response status code.
|
||||
// withQueryTokenAuth 校验 GET 请求中的 token 查询参数,失败时返回 401。
|
||||
func withQueryTokenAuth(expected string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 空 token 一律拒绝,配合配置校验避免无鉴权暴露主机状态。
|
||||
if !queryTokenValid(r.URL.Query().Get("token"), expected) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{
|
||||
"error": "unauthorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// queryTokenValid 以恒定时间比较查询参数 token 与配置值,避免时序侧信道。
|
||||
func queryTokenValid(got, want string) bool {
|
||||
if got == "" || want == "" {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1
|
||||
}
|
||||
|
||||
// writeJSON 以给定状态码将 value 序列化为 JSON 响应;编码失败时连接会被动结束。
|
||||
func writeJSON(w http.ResponseWriter, code int, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
|
||||
@@ -10,15 +10,22 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// TestStatusReturnsCachedSnapshot verifies /status reads the provider cache.
|
||||
const testToken = "test-token"
|
||||
|
||||
// withToken 为请求 URL 附加鉴权查询参数。
|
||||
func withToken(path string) string {
|
||||
return path + "?token=" + testToken
|
||||
}
|
||||
|
||||
// TestStatusReturnsCachedSnapshot 验证 /status 读取提供者缓存。
|
||||
func TestStatusReturnsCachedSnapshot(t *testing.T) {
|
||||
provider := StaticProvider{Snapshot: status.HostStatus{
|
||||
Agent: status.AgentInfo{Version: "test", SnapshotAt: time.Now(), SnapshotStatus: "ok"},
|
||||
Host: status.HostInfo{Hostname: "host-a"},
|
||||
}, OK: true}
|
||||
server := New(provider)
|
||||
server := New(provider, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/status", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, withToken("/status"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -34,14 +41,14 @@ func TestStatusReturnsCachedSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthReportsSnapshotMetadata verifies /health returns snapshot metadata.
|
||||
// TestHealthReportsSnapshotMetadata 验证 /health 返回快照元数据。
|
||||
func TestHealthReportsSnapshotMetadata(t *testing.T) {
|
||||
provider := StaticProvider{Snapshot: status.HostStatus{
|
||||
Agent: status.AgentInfo{Version: "test", SnapshotAt: time.Unix(10, 0), SnapshotStatus: "partial"},
|
||||
}, OK: true}
|
||||
server := New(provider)
|
||||
server := New(provider, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, withToken("/health"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -57,11 +64,11 @@ func TestHealthReportsSnapshotMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusReturnsServiceUnavailableBeforeSnapshot verifies /status does not live collect.
|
||||
// TestStatusReturnsServiceUnavailableBeforeSnapshot 验证 /status 不会在请求时实时采集。
|
||||
func TestStatusReturnsServiceUnavailableBeforeSnapshot(t *testing.T) {
|
||||
server := New(StaticProvider{OK: false})
|
||||
server := New(StaticProvider{OK: false}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/status", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, withToken("/status"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -70,11 +77,11 @@ func TestStatusReturnsServiceUnavailableBeforeSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthReturnsStartingBeforeSnapshot verifies /health exposes startup state.
|
||||
// TestHealthReturnsStartingBeforeSnapshot 验证 /health 在启动阶段暴露 starting 状态。
|
||||
func TestHealthReturnsStartingBeforeSnapshot(t *testing.T) {
|
||||
server := New(StaticProvider{OK: false})
|
||||
server := New(StaticProvider{OK: false}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, withToken("/health"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -90,11 +97,11 @@ func TestHealthReturnsStartingBeforeSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusRejectsNonGET verifies /status only accepts GET requests.
|
||||
// TestStatusRejectsNonGET 验证 /status 仅接受 GET 请求。
|
||||
func TestStatusRejectsNonGET(t *testing.T) {
|
||||
server := New(StaticProvider{OK: true})
|
||||
server := New(StaticProvider{OK: true}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/status", nil)
|
||||
req := httptest.NewRequest(http.MethodPost, withToken("/status"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -103,11 +110,11 @@ func TestStatusRejectsNonGET(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthRejectsNonGET verifies /health only accepts GET requests.
|
||||
// TestHealthRejectsNonGET 验证 /health 仅接受 GET 请求。
|
||||
func TestHealthRejectsNonGET(t *testing.T) {
|
||||
server := New(StaticProvider{OK: true})
|
||||
server := New(StaticProvider{OK: true}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/health", nil)
|
||||
req := httptest.NewRequest(http.MethodPost, withToken("/health"), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
@@ -115,3 +122,29 @@ func TestHealthRejectsNonGET(t *testing.T) {
|
||||
t.Fatalf("status code = %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissingTokenReturnsUnauthorized 验证缺少 token 时返回 401。
|
||||
func TestMissingTokenReturnsUnauthorized(t *testing.T) {
|
||||
server := New(StaticProvider{OK: true}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/status", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status code = %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrongTokenReturnsUnauthorized 验证错误 token 时返回 401。
|
||||
func TestWrongTokenReturnsUnauthorized(t *testing.T) {
|
||||
server := New(StaticProvider{OK: true}, testToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/status?token=wrong", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
server.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status code = %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,22 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// defaultRetryDelay 是单次上报失败后重试前等待的默认时长。
|
||||
const defaultRetryDelay = 2 * time.Second
|
||||
|
||||
// Provider supplies the cached HostStatus used for outbound reports.
|
||||
// Provider 提供用于外发上报的缓存 HostStatus。
|
||||
type Provider interface {
|
||||
// Get returns the latest snapshot and whether a snapshot is ready.
|
||||
// Get 返回最新快照及快照是否已就绪。
|
||||
Get() (status.HostStatus, bool)
|
||||
}
|
||||
|
||||
// Client posts HostStatus snapshots to a configured center endpoint.
|
||||
// Client 将 HostStatus 快照 POST 到配置的中心端地址。
|
||||
type Client struct {
|
||||
endpoint string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a reporting client with the provided endpoint and request timeout.
|
||||
// NewClient 使用给定端点与请求超时创建上报客户端。
|
||||
func NewClient(endpoint string, timeout time.Duration) *Client {
|
||||
return &Client{
|
||||
endpoint: endpoint,
|
||||
@@ -34,7 +35,8 @@ func NewClient(endpoint string, timeout time.Duration) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Report posts one HostStatus snapshot as JSON and returns non-2xx responses as errors.
|
||||
// Report 将一份 HostStatus 以 JSON POST 上报;非 2xx 响应作为错误返回。
|
||||
// 请求超时由 Client 内部的 http.Client.Timeout 控制。
|
||||
func (c *Client) Report(snapshot status.HostStatus) error {
|
||||
body, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
@@ -56,12 +58,12 @@ func (c *Client) Report(snapshot status.HostStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run periodically posts the latest cached snapshot until ctx is canceled.
|
||||
// Run 在 ctx 取消前按间隔 POST 最新缓存快照。
|
||||
func Run(ctx context.Context, provider Provider, client *Client, interval time.Duration, logger *slog.Logger) {
|
||||
runWithRetryDelay(ctx, provider, client, interval, defaultRetryDelay, logger)
|
||||
}
|
||||
|
||||
// runWithRetryDelay runs the reporting loop and retries one failed report after retryDelay.
|
||||
// runWithRetryDelay 运行上报循环;单次失败后按 retryDelay 重试一次。
|
||||
func runWithRetryDelay(ctx context.Context, provider Provider, client *Client, interval time.Duration, retryDelay time.Duration, logger *slog.Logger) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
@@ -70,23 +72,27 @@ func runWithRetryDelay(ctx context.Context, provider Provider, client *Client, i
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 上报只读取缓存,避免与 HTTP 查询之外再触发一轮昂贵采集。
|
||||
snapshot, ok := provider.Get()
|
||||
if !ok {
|
||||
// 首次快照未就绪时跳过本轮,上报循环继续等待下一周期。
|
||||
continue
|
||||
}
|
||||
if err := reportWithRetry(ctx, client, snapshot, retryDelay); err != nil {
|
||||
// 上报失败只写日志,不影响本地 HTTP 服务与后续采集。
|
||||
logger.Warn("status report failed", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reportWithRetry posts a snapshot and performs one cancellation-aware retry on failure.
|
||||
// reportWithRetry 上报快照;失败时在可取消的等待后重试一次。
|
||||
func reportWithRetry(ctx context.Context, client *Client, snapshot status.HostStatus, retryDelay time.Duration) error {
|
||||
err := client.Report(snapshot)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// 重试等待受 ctx 控制,agent 退出时不会被固定退避时间拖住。
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
||||
@@ -14,18 +14,18 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// StaticProvider is a test provider with a fixed cached snapshot.
|
||||
// StaticProvider 是用于测试的固定缓存快照提供者。
|
||||
type StaticProvider struct {
|
||||
Snapshot status.HostStatus
|
||||
OK bool
|
||||
}
|
||||
|
||||
// Get returns the configured static snapshot and readiness flag.
|
||||
// Get 返回配置的静态快照与就绪标志。
|
||||
func (p StaticProvider) Get() (status.HostStatus, bool) {
|
||||
return p.Snapshot, p.OK
|
||||
}
|
||||
|
||||
// TestReportPostsHostStatusJSON verifies reports reuse the HostStatus JSON payload.
|
||||
// TestReportPostsHostStatusJSON 验证上报复用 HostStatus JSON 载荷。
|
||||
func TestReportPostsHostStatusJSON(t *testing.T) {
|
||||
var got status.HostStatus
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -52,7 +52,7 @@ func TestReportPostsHostStatusJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestReportReturnsErrorForHTTPFailure verifies non-2xx responses fail one report.
|
||||
// TestReportReturnsErrorForHTTPFailure 验证非 2xx 响应导致单次上报失败。
|
||||
func TestReportReturnsErrorForHTTPFailure(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -65,7 +65,7 @@ func TestReportReturnsErrorForHTTPFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunRetriesFailedReportOnce verifies the loop retries one failed report.
|
||||
// TestRunRetriesFailedReportOnce 验证循环对失败上报重试一次。
|
||||
func TestRunRetriesFailedReportOnce(t *testing.T) {
|
||||
var requests atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -99,7 +99,7 @@ func TestRunRetriesFailedReportOnce(t *testing.T) {
|
||||
<-done
|
||||
}
|
||||
|
||||
// TestRunStopsDuringRetryDelayWhenContextCanceled verifies retry waits honor cancellation.
|
||||
// TestRunStopsDuringRetryDelayWhenContextCanceled 验证重试等待可被 ctx 取消。
|
||||
func TestRunStopsDuringRetryDelayWhenContextCanceled(t *testing.T) {
|
||||
var requests atomic.Int32
|
||||
firstRequest := make(chan struct{})
|
||||
|
||||
@@ -2,25 +2,25 @@ package service
|
||||
|
||||
import "mfd-monitor-agent/internal/config"
|
||||
|
||||
// InstallOptions carries platform-specific service installation inputs.
|
||||
// InstallOptions 携带平台相关的服务安装参数。
|
||||
type InstallOptions struct {
|
||||
// ConfigPath is preserved in the service command line when a config file was supplied.
|
||||
// ConfigPath 在提供了配置文件时,会写入服务命令行中的 -config 参数。
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// Manager controls operating system service installation and lifecycle commands.
|
||||
// Manager 控制操作系统服务的安装与生命周期命令。
|
||||
type Manager interface {
|
||||
// Install registers the agent executable as an operating system service.
|
||||
// Install 将 agent 可执行文件注册为操作系统服务。
|
||||
Install(executable string, cfg config.Config, opts InstallOptions) error
|
||||
// Uninstall removes the operating system service registration.
|
||||
// Uninstall 移除操作系统服务注册。
|
||||
Uninstall(cfg config.Config) error
|
||||
// Start asks the operating system service manager to start the agent service.
|
||||
// Start 请求操作系统服务管理器启动 agent 服务。
|
||||
Start(cfg config.Config) error
|
||||
// Stop asks the operating system service manager to stop the agent service.
|
||||
// Stop 请求操作系统服务管理器停止 agent 服务。
|
||||
Stop(cfg config.Config) error
|
||||
}
|
||||
|
||||
// NewManager returns the platform-specific service manager.
|
||||
// NewManager 返回平台相关的服务管理器。
|
||||
func NewManager() Manager {
|
||||
return newManager()
|
||||
}
|
||||
|
||||
@@ -11,26 +11,29 @@ import (
|
||||
"mfd-monitor-agent/internal/config"
|
||||
)
|
||||
|
||||
// currentUID 是测试可替换的当前用户 ID 查询函数。
|
||||
var currentUID = os.Geteuid
|
||||
|
||||
// linuxManager manages a systemd service for the agent.
|
||||
// linuxManager 通过 systemd 管理 agent 服务。
|
||||
type linuxManager struct{}
|
||||
|
||||
// newManager returns the Linux systemd-backed service manager.
|
||||
// newManager 返回基于 Linux systemd 的服务管理器。
|
||||
func newManager() Manager {
|
||||
return linuxManager{}
|
||||
}
|
||||
|
||||
// Install writes a systemd unit, reloads systemd, and enables the configured service.
|
||||
// Install 写入 systemd unit、重载 systemd 并 enable 配置的服务。
|
||||
func (linuxManager) Install(executable string, cfg config.Config, opts InstallOptions) error {
|
||||
if currentUID() != 0 {
|
||||
return fmt.Errorf("install requires root")
|
||||
}
|
||||
unit := renderSystemdUnit(executable, opts.ConfigPath, cfg)
|
||||
path := "/etc/systemd/system/" + cfg.Service.Name + ".service"
|
||||
// 安装阶段写入 systemd unit,需要 root 权限且会影响系统服务注册。
|
||||
if err := os.WriteFile(path, []byte(unit), 0644); err != nil {
|
||||
return fmt.Errorf("write systemd unit: %w", err)
|
||||
}
|
||||
// unit 文件变化后必须重载 systemd,再启用服务随系统启动。
|
||||
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
|
||||
return fmt.Errorf("systemctl daemon-reload: %w", err)
|
||||
}
|
||||
@@ -40,10 +43,11 @@ func (linuxManager) Install(executable string, cfg config.Config, opts InstallOp
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderSystemdUnit builds the Linux systemd unit used by Install.
|
||||
// renderSystemdUnit 生成 Install 使用的 Linux systemd unit 内容。
|
||||
func renderSystemdUnit(executable string, configPath string, cfg config.Config) string {
|
||||
args := []string{executable, "run"}
|
||||
if configPath != "" {
|
||||
// install 接收的 -config 会固化到 ExecStart,保证服务模式读取同一配置。
|
||||
args = append(args, "-config", configPath)
|
||||
}
|
||||
return fmt.Sprintf(`[Unit]
|
||||
@@ -62,8 +66,9 @@ WantedBy=multi-user.target
|
||||
`, cfg.Service.Description, strings.Join(args, " "))
|
||||
}
|
||||
|
||||
// Uninstall disables the service, removes its unit file, and reloads systemd.
|
||||
// Uninstall 禁用服务、删除 unit 文件并重载 systemd。
|
||||
func (linuxManager) Uninstall(cfg config.Config) error {
|
||||
// 服务可能已经停止或未启用,忽略 disable 错误以继续清理 unit 文件。
|
||||
_ = exec.Command("systemctl", "disable", "--now", cfg.Service.Name).Run()
|
||||
if err := os.Remove("/etc/systemd/system/" + cfg.Service.Name + ".service"); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
@@ -71,12 +76,12 @@ func (linuxManager) Uninstall(cfg config.Config) error {
|
||||
return exec.Command("systemctl", "daemon-reload").Run()
|
||||
}
|
||||
|
||||
// Start asks systemd to start the configured service.
|
||||
// Start 请求 systemd 启动配置的服务。
|
||||
func (linuxManager) Start(cfg config.Config) error {
|
||||
return exec.Command("systemctl", "start", cfg.Service.Name).Run()
|
||||
}
|
||||
|
||||
// Stop asks systemd to stop the configured service.
|
||||
// Stop 请求 systemd 停止配置的服务。
|
||||
func (linuxManager) Stop(cfg config.Config) error {
|
||||
return exec.Command("systemctl", "stop", cfg.Service.Name).Run()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"mfd-monitor-agent/internal/config"
|
||||
)
|
||||
|
||||
// TestRenderSystemdUnitIncludesConfigPath verifies install preserves the config flag.
|
||||
// TestRenderSystemdUnitIncludesConfigPath 验证安装时保留 -config 参数。
|
||||
func TestRenderSystemdUnitIncludesConfigPath(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
cfg.Service.Description = "MFD Agent"
|
||||
@@ -21,7 +21,7 @@ func TestRenderSystemdUnitIncludesConfigPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinuxInstallRequiresRoot verifies Linux install rejects non-root callers.
|
||||
// TestLinuxInstallRequiresRoot 验证 Linux 安装拒绝非 root 调用。
|
||||
func TestLinuxInstallRequiresRoot(t *testing.T) {
|
||||
oldUID := currentUID
|
||||
t.Cleanup(func() { currentUID = oldUID })
|
||||
|
||||
@@ -9,30 +9,30 @@ import (
|
||||
"mfd-monitor-agent/internal/config"
|
||||
)
|
||||
|
||||
// unsupportedManager reports service lifecycle operations as unsupported on this platform.
|
||||
// unsupportedManager 在本平台将服务生命周期操作报告为不支持。
|
||||
type unsupportedManager struct{}
|
||||
|
||||
// newManager returns a manager that fails all service operations on unsupported platforms.
|
||||
// newManager 返回在不支持平台上对所有服务操作均失败的管理器。
|
||||
func newManager() Manager {
|
||||
return unsupportedManager{}
|
||||
}
|
||||
|
||||
// Install reports that service installation is unsupported on this platform.
|
||||
// Install 报告本平台不支持服务安装。
|
||||
func (unsupportedManager) Install(string, config.Config, InstallOptions) error {
|
||||
return fmt.Errorf("service install is not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// Uninstall reports that service removal is unsupported on this platform.
|
||||
// Uninstall 报告本平台不支持服务卸载。
|
||||
func (unsupportedManager) Uninstall(config.Config) error {
|
||||
return fmt.Errorf("service uninstall is not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// Start reports that service start is unsupported on this platform.
|
||||
// Start 报告本平台不支持服务启动。
|
||||
func (unsupportedManager) Start(config.Config) error {
|
||||
return fmt.Errorf("service start is not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// Stop reports that service stop is unsupported on this platform.
|
||||
// Stop 报告本平台不支持服务停止。
|
||||
func (unsupportedManager) Stop(config.Config) error {
|
||||
return fmt.Errorf("service stop is not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
@@ -9,35 +9,38 @@ import (
|
||||
"mfd-monitor-agent/internal/config"
|
||||
)
|
||||
|
||||
// windowsManager manages the agent through the Windows Service Control Manager.
|
||||
// windowsManager 通过 Windows 服务控制管理器(SCM)管理 agent。
|
||||
type windowsManager struct{}
|
||||
|
||||
// newManager returns the Windows service manager.
|
||||
// newManager 返回 Windows 服务管理器。
|
||||
func newManager() Manager {
|
||||
return windowsManager{}
|
||||
}
|
||||
|
||||
// Install registers the agent executable as an auto-start Windows Service.
|
||||
// Install 将 agent 可执行文件注册为自动启动的 Windows 服务。
|
||||
func (windowsManager) Install(executable string, cfg config.Config, opts InstallOptions) error {
|
||||
args := []string{`"` + executable + `"`, "run"}
|
||||
if opts.ConfigPath != "" {
|
||||
// -config 被写入 binPath,确保 SCM 启动的 run 子命令读取指定配置。
|
||||
args = append(args, "-config", `"`+opts.ConfigPath+`"`)
|
||||
}
|
||||
// sc.exe 的 binPath= 参数要求等号后单独一个参数,因此这里保持分段传参。
|
||||
return exec.Command("sc.exe", "create", cfg.Service.Name, "binPath=", strings.Join(args, " "), "DisplayName=", cfg.Service.DisplayName, "start=", "auto").Run()
|
||||
}
|
||||
|
||||
// Uninstall stops and removes the configured Windows Service.
|
||||
// Uninstall 停止并删除已配置的 Windows 服务。
|
||||
func (windowsManager) Uninstall(cfg config.Config) error {
|
||||
// 服务可能未运行,忽略 stop 错误并继续删除服务注册。
|
||||
_ = exec.Command("sc.exe", "stop", cfg.Service.Name).Run()
|
||||
return exec.Command("sc.exe", "delete", cfg.Service.Name).Run()
|
||||
}
|
||||
|
||||
// Start asks the Windows Service Control Manager to start the service.
|
||||
// Start 请求 SCM 启动服务。
|
||||
func (windowsManager) Start(cfg config.Config) error {
|
||||
return exec.Command("sc.exe", "start", cfg.Service.Name).Run()
|
||||
}
|
||||
|
||||
// Stop asks the Windows Service Control Manager to stop the service.
|
||||
// Stop 请求 SCM 停止服务。
|
||||
func (windowsManager) Stop(cfg config.Config) error {
|
||||
return exec.Command("sc.exe", "stop", cfg.Service.Name).Run()
|
||||
}
|
||||
|
||||
@@ -8,43 +8,48 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// Collector gathers one subsection of HostStatus into the provided snapshot.
|
||||
// Collector 将 HostStatus 的一个子区块采集并写入提供的快照。
|
||||
type Collector interface {
|
||||
// Name returns the stable collector name used in partial snapshot errors.
|
||||
// Name 返回用于 partial 快照错误记录的稳定采集器名称。
|
||||
Name() string
|
||||
// Collect fills its subsection of out and returns an error when that subsection is incomplete.
|
||||
// Collect 填充 out 中对应子区块;该子区块不完整时返回错误。
|
||||
Collect(context.Context, *status.HostStatus) error
|
||||
}
|
||||
|
||||
// CollectorFunc adapts a function into a Collector for tests and small collectors.
|
||||
// CollectorFunc 将函数适配为 Collector,供测试与小型采集器使用。
|
||||
type CollectorFunc func(context.Context, *status.HostStatus) error
|
||||
|
||||
// Name returns a generic collector name for function-backed collectors.
|
||||
// Name 为基于函数的采集器返回通用名称。
|
||||
func (fn CollectorFunc) Name() string {
|
||||
return "collector"
|
||||
}
|
||||
|
||||
// Collect calls the wrapped function.
|
||||
// Collect 调用包装的采集函数。
|
||||
func (fn CollectorFunc) Collect(ctx context.Context, out *status.HostStatus) error {
|
||||
return fn(ctx, out)
|
||||
}
|
||||
|
||||
// Cache stores the most recent HostStatus snapshot behind a concurrency-safe interface.
|
||||
// Cache 在并发安全接口后保存最近一次 HostStatus 快照。
|
||||
type Cache struct {
|
||||
// collectors 是按固定顺序执行的子采集器集合。
|
||||
collectors []Collector
|
||||
version string
|
||||
startedAt time.Time
|
||||
mu sync.RWMutex
|
||||
last status.HostStatus
|
||||
hasLast bool
|
||||
// version 和 startedAt 会写入每次快照的 agent 元数据。
|
||||
version string
|
||||
startedAt time.Time
|
||||
// mu 保护 last 和 hasLast,允许 HTTP 与上报并发读取缓存。
|
||||
mu sync.RWMutex
|
||||
// last 保存最近一次完整或 partial 的 HostStatus。
|
||||
last status.HostStatus
|
||||
// hasLast 表示首次采集是否已经完成。
|
||||
hasLast bool
|
||||
}
|
||||
|
||||
// NewCache creates an empty snapshot cache using the provided collectors and agent metadata.
|
||||
// NewCache 使用给定采集器与 agent 元数据创建空的快照缓存。
|
||||
func NewCache(collectors []Collector, version string, startedAt time.Time) *Cache {
|
||||
return &Cache{collectors: collectors, version: version, startedAt: startedAt}
|
||||
}
|
||||
|
||||
// Collect runs all collectors once and stores the resulting snapshot even when some collectors fail.
|
||||
// Collect 运行全部采集器一次并存储结果;部分采集器失败时仍保存快照。
|
||||
func (c *Cache) Collect(ctx context.Context) status.HostStatus {
|
||||
out := status.HostStatus{
|
||||
Agent: status.AgentInfo{
|
||||
@@ -59,6 +64,7 @@ func (c *Cache) Collect(ctx context.Context) status.HostStatus {
|
||||
|
||||
for _, collector := range c.collectors {
|
||||
if err := collector.Collect(ctx, &out); err != nil {
|
||||
// 单个子采集器失败只让整份快照降级为 partial,其他已成功字段继续对外可用。
|
||||
out.Agent.SnapshotStatus = "partial"
|
||||
out.Errors = append(out.Errors, status.StatusError{
|
||||
Collector: collector.Name(),
|
||||
@@ -67,6 +73,7 @@ func (c *Cache) Collect(ctx context.Context) status.HostStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// 发布快照时一次性替换缓存,避免读者看到半写入的结构。
|
||||
c.mu.Lock()
|
||||
c.last = out
|
||||
c.hasLast = true
|
||||
@@ -74,15 +81,16 @@ func (c *Cache) Collect(ctx context.Context) status.HostStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// Get returns the latest cached snapshot and whether collection has completed at least once.
|
||||
// Get 返回最新缓存快照,以及是否至少完成过一次采集。
|
||||
func (c *Cache) Get() (status.HostStatus, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.last, c.hasLast
|
||||
}
|
||||
|
||||
// Run collects immediately and then refreshes the cache until ctx is canceled.
|
||||
// Run 立即采集一次,随后在 ctx 取消前按间隔刷新缓存。
|
||||
func (c *Cache) Run(ctx context.Context, interval time.Duration, timeout time.Duration) {
|
||||
// 启动后先同步完成一轮采集,让 /health 和 /status 尽快进入就绪状态。
|
||||
c.collectWithTimeout(ctx, timeout)
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
@@ -96,8 +104,9 @@ func (c *Cache) Run(ctx context.Context, interval time.Duration, timeout time.Du
|
||||
}
|
||||
}
|
||||
|
||||
// collectWithTimeout runs one collection cycle with an overall deadline.
|
||||
// collectWithTimeout 在整体截止时间下执行一轮采集。
|
||||
func (c *Cache) collectWithTimeout(ctx context.Context, timeout time.Duration) {
|
||||
// 超时通过 context 传递给子采集器;无法被 context 打断的系统调用仍依赖平台返回。
|
||||
collectCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
c.Collect(collectCtx)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"mfd-monitor-agent/internal/status"
|
||||
)
|
||||
|
||||
// TestCollectorFailuresReturnPartialSnapshot verifies failed collectors do not fail snapshots.
|
||||
// TestCollectorFailuresReturnPartialSnapshot 验证采集器失败不会导致整份快照失败。
|
||||
func TestCollectorFailuresReturnPartialSnapshot(t *testing.T) {
|
||||
cache := NewCache([]Collector{
|
||||
CollectorFunc(func(context.Context, *status.HostStatus) error {
|
||||
@@ -30,7 +30,7 @@ func TestCollectorFailuresReturnPartialSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCacheStoresLastSnapshot verifies successful collection is cached.
|
||||
// TestCacheStoresLastSnapshot 验证成功采集结果被缓存。
|
||||
func TestCacheStoresLastSnapshot(t *testing.T) {
|
||||
cache := NewCache([]Collector{
|
||||
CollectorFunc(func(_ context.Context, out *status.HostStatus) error {
|
||||
@@ -50,7 +50,7 @@ func TestCacheStoresLastSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCollectsImmediately verifies Run collects before the first interval tick.
|
||||
// TestRunCollectsImmediately 验证 Run 在首个间隔 tick 之前立即采集。
|
||||
func TestRunCollectsImmediately(t *testing.T) {
|
||||
collected := make(chan struct{})
|
||||
cache := NewCache([]Collector{
|
||||
@@ -79,7 +79,7 @@ func TestRunCollectsImmediately(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectWithTimeoutMarksCanceledCollectorPartial verifies timeout errors are partial.
|
||||
// TestCollectWithTimeoutMarksCanceledCollectorPartial 验证超时被记为 partial。
|
||||
func TestCollectWithTimeoutMarksCanceledCollectorPartial(t *testing.T) {
|
||||
cache := NewCache([]Collector{
|
||||
CollectorFunc(func(ctx context.Context, _ *status.HostStatus) error {
|
||||
|
||||
@@ -2,147 +2,194 @@ package status
|
||||
|
||||
import "time"
|
||||
|
||||
// HostStatus is the full cached host snapshot returned by /status and posted by the reporter.
|
||||
// HostStatus 是 /status 返回的完整缓存主机快照,也是上报器 POST 的载荷结构。
|
||||
type HostStatus struct {
|
||||
// Agent describes the agent process and most recent snapshot metadata.
|
||||
Agent AgentInfo `json:"agent"`
|
||||
Host HostInfo `json:"host"`
|
||||
Network NetworkInfo `json:"network"`
|
||||
CPU CPUInfo `json:"cpu"`
|
||||
Memory MemoryInfo `json:"memory"`
|
||||
Disks []DiskInfo `json:"disks"`
|
||||
Processes ProcessResult `json:"processes"`
|
||||
TCPListeners TCPResult `json:"tcp_listeners"`
|
||||
// Errors records collector-level failures that caused a partial snapshot.
|
||||
// Agent 描述 agent 进程及最近一次快照的元数据。
|
||||
Agent AgentInfo `json:"agent"`
|
||||
// Host 描述被监控主机的身份、平台与运行时长。
|
||||
Host HostInfo `json:"host"`
|
||||
// Network 汇总网卡、IP 与 MAC 地址信息。
|
||||
Network NetworkInfo `json:"network"`
|
||||
// CPU 描述主机 CPU 拓扑与最近一次采样利用率。
|
||||
CPU CPUInfo `json:"cpu"`
|
||||
// Memory 描述主机内存容量与使用率。
|
||||
Memory MemoryInfo `json:"memory"`
|
||||
// Disks 列出已挂载且被纳入监控的文件系统容量数据。
|
||||
Disks []DiskInfo `json:"disks"`
|
||||
// Processes 包含进程采集状态与完整进程列表。
|
||||
Processes ProcessResult `json:"processes"`
|
||||
// TCPListeners 包含 TCP 监听端口采集状态与端口到进程的映射。
|
||||
TCPListeners TCPResult `json:"tcp_listeners"`
|
||||
// Errors 记录导致快照变为 partial 的子采集器失败信息。
|
||||
Errors []StatusError `json:"errors"`
|
||||
}
|
||||
|
||||
// AgentInfo contains agent process metadata and the latest collection result.
|
||||
// AgentInfo 包含 agent 进程元数据及最近一次采集结果。
|
||||
type AgentInfo struct {
|
||||
Version string `json:"version"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// Version 是当前 agent 二进制版本。
|
||||
Version string `json:"version"`
|
||||
// StartedAt 是 agent 进程启动时间(UTC)。
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// SnapshotAt 是最近一次快照开始构造的时间(UTC)。
|
||||
SnapshotAt time.Time `json:"snapshot_at"`
|
||||
// SnapshotStatus is ok when all collectors succeed and partial when one or more fail.
|
||||
// SnapshotStatus 在所有采集器成功时为 ok,任一失败时为 partial。
|
||||
SnapshotStatus string `json:"snapshot_status"`
|
||||
}
|
||||
|
||||
// HostInfo describes the monitored host identity and operating system.
|
||||
// HostInfo 描述被监控主机的标识与操作系统信息。
|
||||
type HostInfo struct {
|
||||
HostID string `json:"host_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Platform string `json:"platform"`
|
||||
// HostID 是配置的机器标识;为空时平台采集器通常回退为主机名。
|
||||
HostID string `json:"host_id"`
|
||||
// Hostname 是操作系统返回的当前主机名。
|
||||
Hostname string `json:"hostname"`
|
||||
// Platform 是 Go 运行时平台名,例如 linux 或 windows。
|
||||
Platform string `json:"platform"`
|
||||
// PlatformVersion 是尽力采集的平台版本说明。
|
||||
PlatformVersion string `json:"platform_version"`
|
||||
Arch string `json:"arch"`
|
||||
// UptimeSeconds is the host uptime in seconds when available.
|
||||
// Arch 是 Go 运行时架构名,例如 amd64。
|
||||
Arch string `json:"arch"`
|
||||
// UptimeSeconds 为主机运行时长(秒),在可用时填充。
|
||||
UptimeSeconds uint64 `json:"uptime_seconds"`
|
||||
}
|
||||
|
||||
// NetworkInfo contains all network interfaces plus flattened IPv4, IPv6, and MAC lists.
|
||||
// NetworkInfo 包含全部网卡及展平后的 IPv4、IPv6、MAC 列表。
|
||||
type NetworkInfo struct {
|
||||
// Interfaces 列出所有可见网卡及其地址。
|
||||
Interfaces []NetworkInterface `json:"interfaces"`
|
||||
IPv4s []string `json:"ipv4s"`
|
||||
IPv6s []string `json:"ipv6s"`
|
||||
MACs []string `json:"macs"`
|
||||
// IPv4s 是从网卡地址展平并排序后的 IPv4 CIDR/地址列表。
|
||||
IPv4s []string `json:"ipv4s"`
|
||||
// IPv6s 是从网卡地址展平并排序后的 IPv6 CIDR/地址列表。
|
||||
IPv6s []string `json:"ipv6s"`
|
||||
// MACs 是去重并排序后的非空 MAC 地址列表。
|
||||
MACs []string `json:"macs"`
|
||||
}
|
||||
|
||||
// NetworkInterface describes one host network interface and its configured addresses.
|
||||
// NetworkInterface 描述一块网卡及其配置的地址。
|
||||
type NetworkInterface struct {
|
||||
// Name 是操作系统中的网卡名称。
|
||||
Name string `json:"name"`
|
||||
MAC string `json:"mac"`
|
||||
// MTU is the interface maximum transmission unit in bytes.
|
||||
MTU int `json:"mtu"`
|
||||
Flags []string `json:"flags"`
|
||||
// MAC 是网卡硬件地址;虚拟或环回接口可能为空。
|
||||
MAC string `json:"mac"`
|
||||
// MTU 为接口最大传输单元(字节)。
|
||||
MTU int `json:"mtu"`
|
||||
// Flags 是操作系统接口标志的稳定字符串列表。
|
||||
Flags []string `json:"flags"`
|
||||
// Addresses 是接口配置的原始地址列表,通常包含 CIDR 前缀。
|
||||
Addresses []string `json:"addresses"`
|
||||
}
|
||||
|
||||
// CPUInfo describes host CPU capacity and current utilization.
|
||||
// CPUInfo 描述主机 CPU 容量与当前利用率。
|
||||
type CPUInfo struct {
|
||||
LogicalCores int `json:"logical_cores"`
|
||||
// LogicalCores 是运行时可见的逻辑 CPU 数。
|
||||
LogicalCores int `json:"logical_cores"`
|
||||
// PhysicalCores 是尽力采集的物理核心数;平台不可用时可能为 0。
|
||||
PhysicalCores int `json:"physical_cores"`
|
||||
// UsagePercent is the best-effort host CPU usage percentage for the latest collection.
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
LoadAvg []float64 `json:"load_avg"`
|
||||
ModelName string `json:"model_name"`
|
||||
// UsagePercent 为最近一次采集得到的尽力主机 CPU 使用率(百分比)。
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
// LoadAvg 是平台支持时的 1/5/15 分钟负载均值。
|
||||
LoadAvg []float64 `json:"load_avg"`
|
||||
// ModelName 是尽力采集的 CPU 型号名称。
|
||||
ModelName string `json:"model_name"`
|
||||
}
|
||||
|
||||
// MemoryInfo describes host memory usage capacity fields in the declared unit.
|
||||
// MemoryInfo 描述主机内存容量与使用情况,数值单位由 Unit 字段声明。
|
||||
type MemoryInfo struct {
|
||||
// Unit declares the unit used by total, used, free, and available.
|
||||
// Unit 声明 total、used、free、available 使用的单位。
|
||||
Unit string `json:"unit"`
|
||||
// Total is total physical memory in the declared unit.
|
||||
// Total 为物理内存总量(与 Unit 一致)。
|
||||
Total uint64 `json:"total"`
|
||||
// Used is memory currently used in the declared unit.
|
||||
// Used 为当前已用内存(与 Unit 一致)。
|
||||
Used uint64 `json:"used"`
|
||||
// Free is memory currently free in the declared unit.
|
||||
// Free 为当前空闲内存(与 Unit 一致)。
|
||||
Free uint64 `json:"free"`
|
||||
// Available is memory available to new workloads in the declared unit.
|
||||
// Available 为可供新负载使用的内存(与 Unit 一致)。
|
||||
Available uint64 `json:"available"`
|
||||
// UsedPercent is the percentage of total memory currently used.
|
||||
// UsedPercent 为已用内存占总量的百分比。
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
// DiskInfo describes one physical or network mounted filesystem and its capacity.
|
||||
// DiskInfo 描述一块已挂载文件系统的容量与使用情况。
|
||||
type DiskInfo struct {
|
||||
Device string `json:"device"`
|
||||
// Device 是文件系统来源设备或盘符。
|
||||
Device string `json:"device"`
|
||||
// MountPoint 是文件系统挂载点或 Windows 盘符根路径。
|
||||
MountPoint string `json:"mount_point"`
|
||||
FSType string `json:"fs_type"`
|
||||
// Unit declares the unit used by total, used, and free.
|
||||
// FSType 是文件系统类型或平台驱动器类型标签。
|
||||
FSType string `json:"fs_type"`
|
||||
// Unit 声明 total、used、free 使用的单位。
|
||||
Unit string `json:"unit"`
|
||||
// Total is filesystem capacity in the declared unit.
|
||||
// Total 为文件系统总容量(与 Unit 一致)。
|
||||
Total uint64 `json:"total"`
|
||||
// Used is consumed filesystem capacity in the declared unit.
|
||||
// Used 为已占用容量(与 Unit 一致)。
|
||||
Used uint64 `json:"used"`
|
||||
// Free is available filesystem capacity in the declared unit.
|
||||
// Free 为可用容量(与 Unit 一致)。
|
||||
Free uint64 `json:"free"`
|
||||
// UsedPercent is the percentage of total filesystem capacity currently used.
|
||||
// UsedPercent 为已用容量占总量的百分比。
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
// ProcessResult contains process collection status and the collected process list.
|
||||
// ProcessResult 包含进程采集状态及进程列表。
|
||||
type ProcessResult struct {
|
||||
// Status is ok, partial, or error depending on process collection completeness.
|
||||
Status string `json:"status"`
|
||||
Items []ProcessInfo `json:"items"`
|
||||
// Status 根据进程采集完整程度为 ok、partial 或 error。
|
||||
Status string `json:"status"`
|
||||
// Items 是最近一次采集到的进程列表;partial 时仍包含成功读取的条目。
|
||||
Items []ProcessInfo `json:"items"`
|
||||
}
|
||||
|
||||
// ProcessInfo describes one operating system process.
|
||||
// ProcessInfo 描述一个操作系统进程。
|
||||
type ProcessInfo struct {
|
||||
PID int `json:"pid"`
|
||||
PPID int `json:"ppid"`
|
||||
Name string `json:"name"`
|
||||
Exe string `json:"exe"`
|
||||
Cmdline string `json:"cmdline"`
|
||||
// PID 是操作系统进程 ID。
|
||||
PID int `json:"pid"`
|
||||
// PPID 是父进程 ID。
|
||||
PPID int `json:"ppid"`
|
||||
// Name 是进程短名称。
|
||||
Name string `json:"name"`
|
||||
// Exe 是可执行文件路径;权限不足时可能为空或退化为短名称。
|
||||
Exe string `json:"exe"`
|
||||
// Cmdline 是完整命令行;权限或平台限制可能导致为空。
|
||||
Cmdline string `json:"cmdline"`
|
||||
// Username 是进程所属用户;权限不足时可能为空。
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
// StartTime is the platform-specific process start timestamp when available.
|
||||
// Status 是平台提供的进程状态标签。
|
||||
Status string `json:"status"`
|
||||
// StartTime 为平台相关的进程启动时间戳,在可用时填充。
|
||||
StartTime int64 `json:"start_time"`
|
||||
// CPUPercent is the best-effort process CPU usage percentage.
|
||||
// CPUPercent 为尽力估算的进程 CPU 使用率(百分比)。
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
// MemoryRSS is resident memory size in bytes.
|
||||
// MemoryRSS 为常驻内存大小(字节)。
|
||||
MemoryRSS uint64 `json:"memory_rss"`
|
||||
// MemoryVMS is virtual memory size in bytes.
|
||||
// MemoryVMS 为虚拟内存大小(字节)。
|
||||
MemoryVMS uint64 `json:"memory_vms"`
|
||||
}
|
||||
|
||||
// TCPResult contains TCP listener collection status and listener entries.
|
||||
// TCPResult 包含 TCP 监听采集状态及监听项列表。
|
||||
type TCPResult struct {
|
||||
// Status is ok, partial, or error depending on listener mapping completeness.
|
||||
Status string `json:"status"`
|
||||
Items []TCPListenerInfo `json:"items"`
|
||||
// Status 根据监听端口到进程映射的完整程度为 ok、partial 或 error。
|
||||
Status string `json:"status"`
|
||||
// Items 是监听 TCP 套接字列表;partial 时可能缺少部分进程映射。
|
||||
Items []TCPListenerInfo `json:"items"`
|
||||
}
|
||||
|
||||
// TCPListenerInfo describes one listening TCP socket and its owning process when known.
|
||||
// TCPListenerInfo 描述一个处于监听状态的 TCP 套接字及所属进程(若已知)。
|
||||
type TCPListenerInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
PID int `json:"pid"`
|
||||
// IP 是本地监听地址。
|
||||
IP string `json:"ip"`
|
||||
// Port 是本地监听端口。
|
||||
Port int `json:"port"`
|
||||
// PID 是监听套接字所属进程 ID;未知时为 0。
|
||||
PID int `json:"pid"`
|
||||
// ProcessName 是 PID 对应的进程名称;映射失败时为空。
|
||||
ProcessName string `json:"process_name"`
|
||||
ProcessExe string `json:"process_exe"`
|
||||
Family string `json:"family"`
|
||||
// ProcessExe 是 PID 对应的可执行路径;权限不足时可能为空。
|
||||
ProcessExe string `json:"process_exe"`
|
||||
// Family 是地址族标签,通常为 ipv4 或 ipv6。
|
||||
Family string `json:"family"`
|
||||
}
|
||||
|
||||
// StatusError records a collector failure without failing the whole snapshot.
|
||||
// StatusError 记录单个子采集器失败,不导致整份快照请求失败。
|
||||
type StatusError struct {
|
||||
// Collector 是失败或降级的子采集器稳定名称。
|
||||
Collector string `json:"collector"`
|
||||
Message string `json:"message"`
|
||||
// Message 是面向排障的错误摘要,不保证适合终端用户展示。
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
27
scripts/build-all.sh
Executable file
27
scripts/build-all.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键构建 Linux + Windows amd64 单二进制产物。
|
||||
#
|
||||
# 环境变量(会传递给子脚本 build-linux.sh):
|
||||
# OUT_DIR 输出目录,默认 <仓库根>/dist
|
||||
# VERSION 写入二进制的版本号,默认 0.1.0
|
||||
# CGO_ENABLED 是否启用 CGO,默认 0
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/build-all.sh
|
||||
# VERSION=1.0.0 ./scripts/build-all.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_LINUX="${ROOT_DIR}/scripts/build-linux.sh"
|
||||
|
||||
export OUT_DIR="${OUT_DIR:-${ROOT_DIR}/dist}"
|
||||
export VERSION="${VERSION:-0.1.0}"
|
||||
export CGO_ENABLED="${CGO_ENABLED:-0}"
|
||||
|
||||
"${BUILD_LINUX}"
|
||||
|
||||
GOOS_TARGET=windows GOARCH_TARGET=amd64 OUTPUT_NAME=agent-windows-amd64.exe \
|
||||
"${BUILD_LINUX}"
|
||||
|
||||
printf 'all platforms built under %s\n' "${OUT_DIR}"
|
||||
@@ -1,4 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 mfd-monitor-agent 单二进制产物。
|
||||
#
|
||||
# 环境变量:
|
||||
# OUT_DIR 输出目录,默认 <仓库根>/dist
|
||||
# VERSION 写入二进制的版本号,默认 0.1.0(通过 -ldflags -X main.version)
|
||||
# GOOS_TARGET 目标 GOOS,默认 linux
|
||||
# GOARCH_TARGET 目标 GOARCH,默认 amd64
|
||||
# OUTPUT_NAME 输出文件名,默认 agent-<GOOS>-<GOARCH>
|
||||
# CGO_ENABLED 是否启用 CGO,默认 0(静态链接友好)
|
||||
#
|
||||
# 交叉编译 Windows 示例:
|
||||
# GOOS_TARGET=windows GOARCH_TARGET=amd64 OUTPUT_NAME=agent-windows-amd64.exe ./scripts/build-linux.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
# 构建 mfd-monitor-agent 单二进制产物(Windows 目标)。
|
||||
#
|
||||
# 环境变量:
|
||||
# OUT_DIR 输出目录,默认 <仓库根>\dist
|
||||
# VERSION 写入二进制的版本号,默认 0.1.0(通过 -ldflags -X main.version)
|
||||
# GOOS_TARGET 目标 GOOS,默认 windows
|
||||
# GOARCH_TARGET 目标 GOARCH,默认 amd64
|
||||
# OUTPUT_NAME 输出文件名,默认 agent-<GOOS>-<GOARCH>.exe
|
||||
# CGO_ENABLED 是否启用 CGO,默认 0
|
||||
#
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RootDir = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
|
||||
Reference in New Issue
Block a user