From 0bd7b3accd23f7f04ac3c57632f68a7a3e530033 Mon Sep 17 00:00:00 2001 From: Odboy Date: Tue, 3 Nov 2026 20:15:27 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=90=88=E5=B9=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- README.sys.md | 29 ++----- internal/executor/builtin.go | 1 - internal/executor/sysinfo.go | 114 ++++++++++++++++++++++++- internal/executor/sysuptime.go | 133 ----------------------------- internal/registry/builtin.go | 21 ++--- internal/registry/registry_test.go | 1 - 7 files changed, 128 insertions(+), 174 deletions(-) delete mode 100644 internal/executor/sysuptime.go diff --git a/README.md b/README.md index e1a7141..151e3ea 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、 | **file** | 文件查找:按大小、修改时间、名称查找文件、查找空文件/空目录 | [README.file.md](README.file.md) | | **svc** | 服务管理:查看状态、启用/禁用、查看日志 | [README.svc.md](README.svc.md) | | **archive** | 压缩文件:自动识别格式并解压 | [README.archive.md](README.archive.md) | -| **sys** | 系统信息:OS、CPU、内存、磁盘一览、运行时间 | [README.sys.md](README.sys.md) | +| **sys** | 系统信息:OS、运行时间、CPU、内存、磁盘一览 | [README.sys.md](README.sys.md) | | **log** | 系统日志:搜索关键字、查看最近日志 | [README.log.md](README.log.md) | | **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) | @@ -205,7 +205,6 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、 | 看最近日志 | `kopsc log last 100` | | 看防火墙状态 | `kopsc fw status` | | 开放/关闭端口 | `kopsc fw allow 8080/tcp --yes` / `kopsc fw deny 8080/tcp --yes` | -| 看系统运行时间 | `kopsc sys uptime` | | 看网卡和 IP | `kopsc net iface` | | 看 TCP 连接统计 | `kopsc net conn` | | 测网速 | `kopsc net speed` | diff --git a/README.sys.md b/README.sys.md index ed0909f..26316c9 100644 --- a/README.sys.md +++ b/README.sys.md @@ -1,18 +1,15 @@ # sys - 系统信息 -一次性查看 OS、CPU、内存、磁盘概况,不用分别跑 `uname`、`lscpu`、`free`、`df`。 +一次性查看 OS、运行时间、CPU、内存、磁盘概况,不用分别跑 `uname`、`lscpu`、`free`、`df`、`uptime`。 ## 命令 | 命令 | 作用 | | --- | --- | -| `kopsc sysinfo` | 显示系统信息一览 | -| `kopsc sys uptime` | 查看系统运行时间和启动时间 | +| `kopsc sysinfo` | 显示系统信息一览(OS、运行时间、CPU、内存、磁盘) | ## 示例 -### sysinfo - ```bash kopsc sysinfo ``` @@ -27,6 +24,12 @@ kopsc sysinfo 主机名: web-01 内核: 5.15.0-91-generic +=== 运行时间 === + +运行时间: 15 天 3 小时 42 分钟 +启动时间: 2026-10-19 08:30:15 +负载均值: 0.15 0.22 0.18 (1/5/15分钟) + === CPU === 逻辑核心: 4 @@ -44,26 +47,12 @@ Filesystem Size Used Avail Use% Mounted on /dev/vda1 50G 22G 28G 44% / ``` -### sys uptime - -```bash -kopsc sys uptime -``` - -输出示例: - -``` -运行时间: 15 天 3 小时 42 分钟 -启动时间: 2026-10-19 08:30:15 -负载均值: 0.15 0.22 0.18 (1/5/15分钟) -``` - ## 平台实现 | 信息 | Linux | macOS | Windows | | --- | --- | --- | --- | | OS | `/etc/os-release` | runtime.GOOS | runtime.GOOS | +| 运行时间 | `/proc/uptime` + `/proc/loadavg` | `sysctl kern.boottime` | `wmic os get LastBootUpTime` | | CPU | `/proc/cpuinfo` | `sysctl` | runtime | | 内存 | `/proc/meminfo` + cgroup | `sysctl hw.memsize` | `wmic` | | 磁盘 | `df -h` | `df -h` | `wmic logicaldisk` | -| 运行时间 | `/proc/uptime` | `sysctl kern.boottime` | `wmic os get LastBootUpTime` | diff --git a/internal/executor/builtin.go b/internal/executor/builtin.go index 2183bfa..e368bf4 100644 --- a/internal/executor/builtin.go +++ b/internal/executor/builtin.go @@ -79,7 +79,6 @@ var builtins = map[string]Builtin{ "svc.enable": runSvcEnable, "svc.logs": runSvcLogs, "svc.status": runSvcStatus, - "sys.uptime": runSysUptime, "sysinfo": runSysInfo, "user.grant": runUserGrant, "user.info": runUserInfo, diff --git a/internal/executor/sysinfo.go b/internal/executor/sysinfo.go index 21a3522..98e4e8c 100644 --- a/internal/executor/sysinfo.go +++ b/internal/executor/sysinfo.go @@ -8,11 +8,12 @@ import ( "runtime" "strconv" "strings" + "time" "gitea.odboy.cn/odboy/kenaito-kopsc/internal/platform" ) -// runSysInfo implements `kopsc sysinfo`: show OS, CPU, memory, disk overview. +// runSysInfo implements `kopsc sysinfo`: show OS, CPU, memory, disk, uptime overview. func runSysInfo(ctx context.Context, req BuiltinRequest) error { w := req.Stdout res := platform.DetectResources() @@ -38,6 +39,10 @@ func runSysInfo(ctx context.Context, req BuiltinRequest) error { } } + // 运行时间 + fmt.Fprintf(w, "\n=== 运行时间 ===\n\n") + printUptime(ctx, w) + fmt.Fprintf(w, "\n=== CPU ===\n\n") fmt.Fprintf(w, "逻辑核心: %d\n", res.CPUs) if runtime.GOOS == "linux" { @@ -67,6 +72,113 @@ func runSysInfo(ctx context.Context, req BuiltinRequest) error { return nil } +// printUptime 打印系统运行时间信息 +func printUptime(ctx context.Context, w interface{ Write([]byte) (int, error) }) { + switch runtime.GOOS { + case "linux": + printUptimeLinux(w) + case "darwin": + printUptimeDarwin(ctx, w) + case "windows": + printUptimeWindows(ctx, w) + } +} + +func printUptimeLinux(w interface{ Write([]byte) (int, error) }) { + data, err := os.ReadFile("/proc/uptime") + if err != nil { + return + } + fields := strings.Fields(string(data)) + if len(fields) < 1 { + return + } + seconds, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return + } + + bootTime := time.Now().Add(-time.Duration(seconds) * time.Second) + fmt.Fprintf(w, "运行时间: %s\n", formatDuration(time.Duration(seconds)*time.Second)) + fmt.Fprintf(w, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) + + // Load average from /proc/loadavg + if loadavg, err := os.ReadFile("/proc/loadavg"); err == nil { + parts := strings.Fields(string(loadavg)) + if len(parts) >= 3 { + fmt.Fprintf(w, "负载均值: %s %s %s (1/5/15分钟)\n", parts[0], parts[1], parts[2]) + } + } +} + +func printUptimeDarwin(ctx context.Context, w interface{ Write([]byte) (int, error) }) { + out, err := exec.CommandContext(ctx, "sysctl", "-n", "kern.boottime").Output() + if err != nil { + return + } + // Output: { sec = 1234567890, usec = 123456 } Mon Jan 01 00:00:00 2024 + line := string(out) + secStart := strings.Index(line, "sec = ") + if secStart < 0 { + return + } + secStr := strings.TrimSpace(line[secStart+6:]) + if idx := strings.Index(secStr, ","); idx >= 0 { + secStr = secStr[:idx] + } + sec, err := strconv.ParseInt(secStr, 10, 64) + if err != nil { + return + } + + bootTime := time.Unix(sec, 0) + uptime := time.Since(bootTime) + fmt.Fprintf(w, "运行时间: %s\n", formatDuration(uptime)) + fmt.Fprintf(w, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) + + // Load average + if out, err := exec.CommandContext(ctx, "sysctl", "-n", "vm.loadavg").Output(); err == nil { + fmt.Fprintf(w, "负载均值: %s\n", strings.TrimSpace(string(out))) + } +} + +func printUptimeWindows(ctx context.Context, w interface{ Write([]byte) (int, error) }) { + out, err := exec.CommandContext(ctx, "wmic", "os", "get", "LastBootUpTime", "/value").Output() + if err != nil { + return + } + line := strings.TrimSpace(string(out)) + if !strings.HasPrefix(line, "LastBootUpTime=") { + return + } + timeStr := strings.TrimPrefix(line, "LastBootUpTime=") + if len(timeStr) < 14 { + return + } + bootTime, err := time.Parse("20060102150405", timeStr[:14]) + if err != nil { + return + } + uptime := time.Since(bootTime) + fmt.Fprintf(w, "运行时间: %s\n", formatDuration(uptime)) + fmt.Fprintf(w, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) +} + +// formatDuration formats a duration into a human-readable string. +func formatDuration(d time.Duration) string { + days := int(d.Hours()) / 24 + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + + if days > 0 { + return fmt.Sprintf("%d 天 %d 小时 %d 分钟", days, hours, minutes) + } + if hours > 0 { + return fmt.Sprintf("%d 小时 %d 分钟", hours, minutes) + } + return fmt.Sprintf("%d 分钟", minutes) +} + func cpuModelLinux() string { data, err := os.ReadFile("/proc/cpuinfo") if err != nil { diff --git a/internal/executor/sysuptime.go b/internal/executor/sysuptime.go deleted file mode 100644 index 34c05fd..0000000 --- a/internal/executor/sysuptime.go +++ /dev/null @@ -1,133 +0,0 @@ -package executor - -import ( - "context" - "fmt" - "os" - "os/exec" - "runtime" - "strconv" - "strings" - "time" -) - -// runSysUptime implements `kopsc sys uptime`: show system uptime. -func runSysUptime(ctx context.Context, req BuiltinRequest) error { - switch runtime.GOOS { - case "linux": - return sysUptimeLinux(req) - case "darwin": - return sysUptimeDarwin(ctx, req) - case "windows": - return sysUptimeWindows(ctx, req) - default: - return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) - } -} - -func sysUptimeLinux(req BuiltinRequest) error { - data, err := os.ReadFile("/proc/uptime") - if err != nil { - return fmt.Errorf("读取 /proc/uptime 失败: %w", err) - } - fields := strings.Fields(string(data)) - if len(fields) < 1 { - return fmt.Errorf("/proc/uptime 格式异常") - } - seconds, err := strconv.ParseFloat(fields[0], 64) - if err != nil { - return fmt.Errorf("解析 uptime 失败: %w", err) - } - - bootTime := time.Now().Add(-time.Duration(seconds) * time.Second) - fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(time.Duration(seconds)*time.Second)) - fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) - - // Load average - if len(fields) >= 2 { - idle, _ := strconv.ParseFloat(fields[1], 64) - fmt.Fprintf(req.Stdout, "空闲时间: %s\n", formatDuration(time.Duration(idle)*time.Second)) - } - - // Load average from /proc/loadavg - if loadavg, err := os.ReadFile("/proc/loadavg"); err == nil { - parts := strings.Fields(string(loadavg)) - if len(parts) >= 3 { - fmt.Fprintf(req.Stdout, "负载均值: %s %s %s (1/5/15分钟)\n", parts[0], parts[1], parts[2]) - } - } - return nil -} - -func sysUptimeDarwin(ctx context.Context, req BuiltinRequest) error { - out, err := exec.CommandContext(ctx, "sysctl", "-n", "kern.boottime").Output() - if err != nil { - return fmt.Errorf("获取启动时间失败: %w", err) - } - // Output: { sec = 1234567890, usec = 123456 } Mon Jan 01 00:00:00 2024 - line := string(out) - secStart := strings.Index(line, "sec = ") - if secStart < 0 { - return fmt.Errorf("无法解析 kern.boottime: %s", line) - } - secStr := strings.TrimSpace(line[secStart+6:]) - if idx := strings.Index(secStr, ","); idx >= 0 { - secStr = secStr[:idx] - } - sec, err := strconv.ParseInt(secStr, 10, 64) - if err != nil { - return fmt.Errorf("解析启动时间失败: %w", err) - } - - bootTime := time.Unix(sec, 0) - uptime := time.Since(bootTime) - fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(uptime)) - fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) - - // Load average - if out, err := exec.CommandContext(ctx, "sysctl", "-n", "vm.loadavg").Output(); err == nil { - fmt.Fprintf(req.Stdout, "负载均值: %s\n", strings.TrimSpace(string(out))) - } - return nil -} - -func sysUptimeWindows(ctx context.Context, req BuiltinRequest) error { - // Use wmic to get boot time - out, err := exec.CommandContext(ctx, "wmic", "os", "get", "LastBootUpTime", "/value").Output() - if err != nil { - return fmt.Errorf("获取启动时间失败: %w", err) - } - line := strings.TrimSpace(string(out)) - // LastBootUpTime=20240101000000.000000+480 - if !strings.HasPrefix(line, "LastBootUpTime=") { - return fmt.Errorf("无法解析 wmic 输出: %s", line) - } - timeStr := strings.TrimPrefix(line, "LastBootUpTime=") - // Parse: 20240101000000.000000+480 - if len(timeStr) < 14 { - return fmt.Errorf("时间格式异常: %s", timeStr) - } - bootTime, err := time.Parse("20060102150405", timeStr[:14]) - if err != nil { - return fmt.Errorf("解析时间失败: %w", err) - } - uptime := time.Since(bootTime) - fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(uptime)) - fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05")) - return nil -} - -// formatDuration formats a duration into a human-readable string. -func formatDuration(d time.Duration) string { - days := int(d.Hours()) / 24 - hours := int(d.Hours()) % 24 - minutes := int(d.Minutes()) % 60 - - if days > 0 { - return fmt.Sprintf("%d 天 %d 小时 %d 分钟", days, hours, minutes) - } - if hours > 0 { - return fmt.Sprintf("%d 小时 %d 分钟", hours, minutes) - } - return fmt.Sprintf("%d 分钟", minutes) -} diff --git a/internal/registry/builtin.go b/internal/registry/builtin.go index a152d1c..f87d021 100644 --- a/internal/registry/builtin.go +++ b/internal/registry/builtin.go @@ -704,8 +704,8 @@ func Builtin() *Registry { { ID: "sysinfo", Group: "sys", - Summary: "显示系统信息一览(OS、CPU、内存、磁盘)", - Action: "一次性输出操作系统、CPU 型号与核心数、内存总量与可用量、磁盘使用情况。" + + Summary: "显示系统信息一览(OS、运行时间、CPU、内存、磁盘)", + Action: "一次性输出操作系统、运行时间、CPU 型号与核心数、内存总量与可用量、磁盘使用情况。" + "Linux 读取 /proc 与 cgroup,macOS 使用 sysctl,Windows 使用 wmic", Variants: threePlatformBuiltins("sysinfo"), }, @@ -837,17 +837,6 @@ func Builtin() *Registry { Variants: threePlatformBuiltins("fw.deny"), }, - // ----- sys additions ----- - { - ID: "sys.uptime", - Group: "sys", - Summary: "查看系统运行时间和启动时间", - Action: "显示系统已运行多长时间、启动时间、负载均值。" + - "Linux: /proc/uptime + /proc/loadavg;macOS: sysctl kern.boottime;" + - "Windows: wmic os get LastBootUpTime", - Variants: threePlatformBuiltins("sys.uptime"), - }, - // ----- net additions ----- { ID: "net.iface", @@ -871,7 +860,7 @@ func Builtin() *Registry { Summary: "简单网速测试(下载/上传)", Action: "从 Cloudflare 下载测试文件测量下载速度,向 httpbin.org 上传数据测量上传速度。" + "默认测试 10MB,--size 可指定大小,--download 或 --upload 可单独测试", - Usage: "[--size 大小] [--download] [--upload]", + Usage: "[--size 大小] [--download] [--upload]", Variants: threePlatformBuiltins("net.speed"), }, @@ -892,7 +881,7 @@ func Builtin() *Registry { Summary: "查找空文件和空目录", Action: "递归扫描目录,找出大小为 0 的文件和没有任何内容的目录。" + "--files-only 只查找空文件,--dirs-only 只查找空目录", - Usage: "[路径] [--files-only] [--dirs-only]", + Usage: "[路径] [--files-only] [--dirs-only]", Variants: threePlatformBuiltins("file.empty"), }, @@ -904,7 +893,7 @@ func Builtin() *Registry { Action: "显示用户的 UID、GID、组、家目录、Shell 等信息。" + "不指定用户名时显示当前用户。" + "Linux/macOS: id + /etc/passwd + groups;Windows: net user", - Usage: "[用户名]", + Usage: "[用户名]", Variants: threePlatformBuiltins("user.info"), }, }} diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go index bfd9972..8c97157 100644 --- a/internal/registry/registry_test.go +++ b/internal/registry/registry_test.go @@ -35,7 +35,6 @@ func TestBuiltinIDsStable(t *testing.T) { "net.ping", "net.trace", "net.dns", "net.curl", "log.search", "log.last", "fw.status", "fw.allow", "fw.deny", - "sys.uptime", "net.iface", "net.conn", "net.speed", "proc.tree", "file.empty",