feat: 新增 sys.uptime/net.iface/net.conn/net.speed/proc.tree/file.empty/user.info 命令

This commit is contained in:
2026-11-03 20:05:10 +08:00
parent 0cf0dd61b9
commit aeeef892ba
16 changed files with 978 additions and 8 deletions
+24 -1
View File
@@ -1,6 +1,6 @@
# file - 文件查找 # file - 文件查找
按大小、修改时间、名称查找文件,不用记 `find` 的复杂语法。 按大小、修改时间、名称查找文件,查找空文件和空目录,不用记 `find` 的复杂语法。
## 命令 ## 命令
@@ -9,6 +9,7 @@
| `kopsc file findbig [路径] [--threshold 大小]` | 查找大于指定阈值的文件,按大小排序 | | `kopsc file findbig [路径] [--threshold 大小]` | 查找大于指定阈值的文件,按大小排序 |
| `kopsc file findrecent [路径] [--days 天数]` | 查找最近 N 天内修改过的文件 | | `kopsc file findrecent [路径] [--days 天数]` | 查找最近 N 天内修改过的文件 |
| `kopsc file findname <模式> [路径]` | 按文件名查找(忽略大小写,支持 `*` `?` 通配符) | | `kopsc file findname <模式> [路径]` | 按文件名查找(忽略大小写,支持 `*` `?` 通配符) |
| `kopsc file empty [路径]` | 查找空文件和空目录 |
## 示例 ## 示例
@@ -51,3 +52,25 @@ kopsc file findname "*.CONF"
| --- | --- | | --- | --- |
| 第一个位置参数 | 文件名模式(必填) | | 第一个位置参数 | 文件名模式(必填) |
| 第二个位置参数 | 搜索路径(默认 `.`) | | 第二个位置参数 | 搜索路径(默认 `.`) |
### empty
```bash
# 查找当前目录下的空文件和空目录
kopsc file empty
# 查找指定目录
kopsc file empty /var/log
# 只查找空文件
kopsc file empty --files-only
# 只查找空目录
kopsc file empty --dirs-only
```
| 选项 | 说明 |
| --- | --- |
| `--files-only` | 只查找空文件 |
| `--dirs-only` | 只查找空目录 |
| 位置参数 | 搜索路径(默认 `.`) |
+12 -5
View File
@@ -143,18 +143,18 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
| 分组 | 用途 | 文档 | | 分组 | 用途 | 文档 |
| --- | --- | --- | | --- | --- | --- |
| **docker** | 容器与镜像:查看、启停、日志、清理、导出/导入、Compose | [README.docker.md](README.docker.md) | | **docker** | 容器与镜像:查看、启停、日志、清理、导出/导入、Compose | [README.docker.md](README.docker.md) |
| **net** | 网络与端口:端口被谁占用、监听端口、ping、DNS、路由追踪、HTTP 诊断 | [README.net.md](README.net.md) | | **net** | 网络与端口:端口被谁占用、监听端口、ping、DNS、路由追踪、HTTP 诊断、网卡信息、连接统计、网速测试 | [README.net.md](README.net.md) |
| **disk** | 磁盘与文件占用:哪个目录、哪个文件最占空间 | [README.disk.md](README.disk.md) | | **disk** | 磁盘与文件占用:哪个目录、哪个文件最占空间 | [README.disk.md](README.disk.md) |
| **diagnose** | 容器 / Pod 状态根因诊断(自动识别 Docker 与 K8s) | [README.diagnose.md](README.diagnose.md) | | **diagnose** | 容器 / Pod 状态根因诊断(自动识别 Docker 与 K8s) | [README.diagnose.md](README.diagnose.md) |
| **proc** | 本机进程:后台运行、等待退出、按资源排序、按名称终止 | [README.proc.md](README.proc.md) | | **proc** | 本机进程:后台运行、等待退出、按资源排序、按名称终止、进程树 | [README.proc.md](README.proc.md) |
| **java** | Java 应用:按 JDK 版本套用调优参数并运行 jar | [README.java.md](README.java.md) | | **java** | Java 应用:按 JDK 版本套用调优参数并运行 jar | [README.java.md](README.java.md) |
| **git** | Git 历史:按日期范围调整提交时间并强推 | [README.git.md](README.git.md) | | **git** | Git 历史:按日期范围调整提交时间并强推 | [README.git.md](README.git.md) |
| **pkg** | 系统软件包:刷新包管理器索引 | [README.pkg.md](README.pkg.md) | | **pkg** | 系统软件包:刷新包管理器索引 | [README.pkg.md](README.pkg.md) |
| **user** | 用户管理:授予或移除用户的 root/sudo 权限 | [README.user.md](README.user.md) | | **user** | 用户管理:授予或移除用户的 root/sudo 权限、查看用户信息 | [README.user.md](README.user.md) |
| **file** | 文件查找:按大小、修改时间、名称查找文件 | [README.file.md](README.file.md) | | **file** | 文件查找:按大小、修改时间、名称查找文件、查找空文件/空目录 | [README.file.md](README.file.md) |
| **svc** | 服务管理:查看状态、启用/禁用、查看日志 | [README.svc.md](README.svc.md) | | **svc** | 服务管理:查看状态、启用/禁用、查看日志 | [README.svc.md](README.svc.md) |
| **archive** | 压缩文件:自动识别格式并解压 | [README.archive.md](README.archive.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) | | **log** | 系统日志:搜索关键字、查看最近日志 | [README.log.md](README.log.md) |
| **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) | | **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) |
@@ -205,6 +205,13 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
| 看最近日志 | `kopsc log last 100` | | 看最近日志 | `kopsc log last 100` |
| 看防火墙状态 | `kopsc fw status` | | 看防火墙状态 | `kopsc fw status` |
| 开放/关闭端口 | `kopsc fw allow 8080/tcp --yes` / `kopsc fw deny 8080/tcp --yes` | | 开放/关闭端口 | `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` |
| 看进程树 | `kopsc proc tree` |
| 找空文件/空目录 | `kopsc file empty /var` |
| 看用户信息 | `kopsc user info` |
## 平台识别 ## 平台识别
+64 -1
View File
@@ -2,7 +2,7 @@
> 返回 [README.md](README.md) > 返回 [README.md](README.md)
查看端口被谁占用、本机监听了哪些端口、测试连通性、DNS 查询、路由追踪、HTTP 诊断。 查看端口被谁占用、本机监听了哪些端口、测试连通性、DNS 查询、路由追踪、HTTP 诊断、网卡信息、连接统计、网速测试。
```bash ```bash
kopsc net pids 8080 # 谁占用了 8080 端口(输出 PID) kopsc net pids 8080 # 谁占用了 8080 端口(输出 PID)
@@ -11,6 +11,9 @@ kopsc net ping example.com # 测试连通性
kopsc net trace example.com # 路由追踪 kopsc net trace example.com # 路由追踪
kopsc net dns example.com # DNS 查询 kopsc net dns example.com # DNS 查询
kopsc net curl https://example.com # HTTP 诊断 kopsc net curl https://example.com # HTTP 诊断
kopsc net iface # 查看网卡和 IP 地址
kopsc net conn # 查看 TCP 连接状态统计
kopsc net speed # 简单网速测试
``` ```
## 端口占用查询(net.pids) ## 端口占用查询(net.pids)
@@ -123,3 +126,63 @@ TLS 握手: 0.089s
| `-L` / `--follow` | 跟随重定向 | | `-L` / `--follow` | 跟随重定向 |
| `-X` / `--method` | HTTP 方法(GET/POST/PUT 等) | | `-X` / `--method` | HTTP 方法(GET/POST/PUT 等) |
| `-H` / `--header` | 添加请求头,可重复 | | `-H` / `--header` | 添加请求头,可重复 |
## 网卡信息(net.iface)
```bash
kopsc net iface
```
列出所有非回环网卡的信息:名称、状态(UP/DOWN)、MAC 地址、IPv4/IPv6 地址、MTU。
纯 Go 实现,跨平台一致,不依赖外部命令。
## 连接统计(net.conn)
```bash
kopsc net conn
```
统计各状态的 TCP 连接数量,输出示例:
```
=== TCP 连接统计 (共 108 个) ===
ESTABLISHED 61
LISTEN 32
TIME_WAIT 14
CLOSE_WAIT 1
```
Linux 优先使用 `ss -tan`(更快),回退到 `netstat -tan`;macOS/Windows 使用 `netstat -tan`。
## 网速测试(net.speed)
```bash
kopsc net speed # 默认测试 10MB
kopsc net speed --size 50M # 测试 50MB
kopsc net speed --download # 只测下载
kopsc net speed --upload # 只测上传
```
从 Cloudflare 下载测试文件测量下载速度,向 httpbin.org 上传数据测量上传速度。
输出示例:
```
=== 网络速度测试 ===
测试下载速度...
下载速度: 12.5M/s
测试上传速度...
上传速度: 3.2M/s
```
### 选项
| 选项 | 说明 |
| --- | --- |
| `--size` / `-s` | 测试文件大小(默认 10M,支持 K/M/G 后缀) |
| `--download` / `-d` | 只测下载 |
| `--upload` / `-u` | 只测上传 |
+12 -1
View File
@@ -3,7 +3,7 @@
> 返回 [README.md](README.md) > 返回 [README.md](README.md)
在后台运行命令并写日志,关掉终端也不会退出;也可以等某个进程退出后再继续; 在后台运行命令并写日志,关掉终端也不会退出;也可以等某个进程退出后再继续;
还能按资源占用排序查看进程、按名称终止进程。 还能按资源占用排序查看进程、按名称终止进程、查看进程树。
```bash ```bash
kopsc proc nohup ./server # 后台运行命令,输出写日志 kopsc proc nohup ./server # 后台运行命令,输出写日志
@@ -11,6 +11,7 @@ kopsc proc wait nginx # 等 nginx 退出后返回
kopsc proc topmem # 内存占用前 10 的进程 kopsc proc topmem # 内存占用前 10 的进程
kopsc proc topcpu # CPU 占用前 10 的进程 kopsc proc topcpu # CPU 占用前 10 的进程
kopsc proc killname nginx --yes # 终止名为 nginx 的进程 kopsc proc killname nginx --yes # 终止名为 nginx 的进程
kopsc proc tree # 查看进程树
``` ```
## 后台运行(proc.nohup) ## 后台运行(proc.nohup)
@@ -128,3 +129,13 @@ kopsc proc killname nginx --yes # 确认终止
匹配规则:进程名或命令行中包含指定关键字(忽略大小写)。 匹配规则:进程名或命令行中包含指定关键字(忽略大小写)。
> 这是破坏性命令,未加 `--yes` 时只列出匹配进程并以退出码 2 结束。 > 这是破坏性命令,未加 `--yes` 时只列出匹配进程并以退出码 2 结束。
## 进程树(proc.tree)
```bash
kopsc proc tree
```
以树形结构显示进程的父子关系。
Linux/macOS 优先使用 `pstree -p`(最佳输出),回退到 `ps -ejH`(Linux)或 `ps -eo pid,ppid,comm`(macOS);Windows 使用 `tasklist /tree`。
+18
View File
@@ -7,9 +7,12 @@
| 命令 | 作用 | | 命令 | 作用 |
| --- | --- | | --- | --- |
| `kopsc sysinfo` | 显示系统信息一览 | | `kopsc sysinfo` | 显示系统信息一览 |
| `kopsc sys uptime` | 查看系统运行时间和启动时间 |
## 示例 ## 示例
### sysinfo
```bash ```bash
kopsc sysinfo kopsc sysinfo
``` ```
@@ -41,6 +44,20 @@ Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 22G 28G 44% / /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 | | 信息 | Linux | macOS | Windows |
@@ -49,3 +66,4 @@ Filesystem Size Used Avail Use% Mounted on
| CPU | `/proc/cpuinfo` | `sysctl` | runtime | | CPU | `/proc/cpuinfo` | `sysctl` | runtime |
| 内存 | `/proc/meminfo` + cgroup | `sysctl hw.memsize` | `wmic` | | 内存 | `/proc/meminfo` + cgroup | `sysctl hw.memsize` | `wmic` |
| 磁盘 | `df -h` | `df -h` | `wmic logicaldisk` | | 磁盘 | `df -h` | `df -h` | `wmic logicaldisk` |
| 运行时间 | `/proc/uptime` | `sysctl kern.boottime` | `wmic os get LastBootUpTime` |
+25
View File
@@ -8,6 +8,7 @@
| --- | --- | --- | | --- | --- | --- |
| `kopsc user grant <用户名>` | 授予用户 root/sudo 权限 | Linux: `usermod -aG wheel/sudo <用户名>`<br>macOS: `dseditgroup -o edit -t user -a <用户名> admin`<br>Windows: `net localgroup Administrators <用户名> /add` | | `kopsc user grant <用户名>` | 授予用户 root/sudo 权限 | Linux: `usermod -aG wheel/sudo <用户名>`<br>macOS: `dseditgroup -o edit -t user -a <用户名> admin`<br>Windows: `net localgroup Administrators <用户名> /add` |
| `kopsc user revoke <用户名>` | 移除用户的 root/sudo 权限 | Linux: `gpasswd -d <用户名> wheel/sudo`<br>macOS: `dseditgroup -o edit -t user -d <用户名> admin`<br>Windows: `net localgroup Administrators <用户名> /delete` | | `kopsc user revoke <用户名>` | 移除用户的 root/sudo 权限 | Linux: `gpasswd -d <用户名> wheel/sudo`<br>macOS: `dseditgroup -o edit -t user -d <用户名> admin`<br>Windows: `net localgroup Administrators <用户名> /delete` |
| `kopsc user info [用户名]` | 查看用户详细信息 | Linux/macOS: `id` + `/etc/passwd` + `groups`<br>Windows: `net user` |
## 用法 ## 用法
@@ -129,3 +130,27 @@ done
**原因**:当前用户没有 sudo/管理员权限 **原因**:当前用户没有 sudo/管理员权限
**解决**:使用已有管理员权限的账户执行,或临时使用 `sudo` 前缀 **解决**:使用已有管理员权限的账户执行,或临时使用 `sudo` 前缀
## 查看用户信息(user.info)
```bash
# 查看当前用户信息
kopsc user info
# 查看指定用户信息
kopsc user info alice
```
输出示例:
```
用户: uid=501(alice) gid=20(staff) groups=20(staff),80(admin),204(_developer)
UID: 501
GID: 20
描述: Alice Wang
家目录: /Users/alice
Shell: /bin/zsh
组: staff admin _developer
```
显示用户的 UID、GID、所属组、描述信息、家目录、默认 Shell 等。不指定用户名时显示当前用户信息。
+7
View File
@@ -47,6 +47,7 @@ var builtins = map[string]Builtin{
"docker.save": runDockerSave, "docker.save": runDockerSave,
"docker.stats": runDockerStats, "docker.stats": runDockerStats,
"docker.stop": runDockerStop, "docker.stop": runDockerStop,
"file.empty": runFileEmpty,
"file.findbig": runFileFindBig, "file.findbig": runFileFindBig,
"file.findname": runFileFindName, "file.findname": runFileFindName,
"file.findrecent": runFileFindRecent, "file.findrecent": runFileFindRecent,
@@ -59,23 +60,29 @@ var builtins = map[string]Builtin{
"java.run.preview": runJavaPreview, "java.run.preview": runJavaPreview,
"log.last": runLogLast, "log.last": runLogLast,
"log.search": runLogSearch, "log.search": runLogSearch,
"net.conn": runNetConn,
"net.curl": runNetCurl, "net.curl": runNetCurl,
"net.dns": runNetDNS, "net.dns": runNetDNS,
"net.iface": runNetIface,
"net.ping": runNetPing, "net.ping": runNetPing,
"net.pids": runNetPIDs, "net.pids": runNetPIDs,
"net.ports": runNetPorts, "net.ports": runNetPorts,
"net.speed": runNetSpeed,
"net.trace": runNetTrace, "net.trace": runNetTrace,
"proc.killname": runProcKillName, "proc.killname": runProcKillName,
"proc.nohup": runNohup, "proc.nohup": runNohup,
"proc.topcpu": runProcTopCPU, "proc.topcpu": runProcTopCPU,
"proc.topmem": runProcTopMem, "proc.topmem": runProcTopMem,
"proc.tree": runProcTree,
"proc.wait": runProcWait, "proc.wait": runProcWait,
"svc.disable": runSvcDisable, "svc.disable": runSvcDisable,
"svc.enable": runSvcEnable, "svc.enable": runSvcEnable,
"svc.logs": runSvcLogs, "svc.logs": runSvcLogs,
"svc.status": runSvcStatus, "svc.status": runSvcStatus,
"sys.uptime": runSysUptime,
"sysinfo": runSysInfo, "sysinfo": runSysInfo,
"user.grant": runUserGrant, "user.grant": runUserGrant,
"user.info": runUserInfo,
"user.revoke": runUserRevoke, "user.revoke": runUserRevoke,
} }
+135
View File
@@ -0,0 +1,135 @@
package executor
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
// runFileEmpty implements `kopsc file empty`: find empty files and directories.
func runFileEmpty(ctx context.Context, req BuiltinRequest) error {
opts, err := parseFileEmptyArgs(req.Args)
if err != nil {
return err
}
root, err := filepath.Abs(opts.path)
if err != nil {
return fmt.Errorf("无法解析路径 %q: %w", opts.path, err)
}
var emptyFiles []string
var emptyDirs []string
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return nil
}
if err := ctx.Err(); err != nil {
return err
}
if d.IsDir() {
// Check if directory is empty
entries, err := os.ReadDir(path)
if err != nil {
return nil
}
if len(entries) == 0 && path != root {
rel, _ := filepath.Rel(root, path)
if rel == "" {
rel = path
}
emptyDirs = append(emptyDirs, rel)
}
return nil
}
// Check if file is empty
info, err := d.Info()
if err != nil {
return nil
}
if info.Size() == 0 {
rel, _ := filepath.Rel(root, path)
if rel == "" {
rel = path
}
emptyFiles = append(emptyFiles, rel)
}
return nil
})
if err != nil {
return err
}
sort.Strings(emptyFiles)
sort.Strings(emptyDirs)
if !opts.dirsOnly {
if len(emptyFiles) == 0 {
fmt.Fprintln(req.Stdout, "没有空文件")
} else {
fmt.Fprintf(req.Stdout, "=== 空文件 (%d 个) ===\n", len(emptyFiles))
for _, f := range emptyFiles {
fmt.Fprintln(req.Stdout, f)
}
fmt.Fprintln(req.Stdout)
}
}
if !opts.filesOnly {
if len(emptyDirs) == 0 {
fmt.Fprintln(req.Stdout, "没有空目录")
} else {
fmt.Fprintf(req.Stdout, "=== 空目录 (%d 个) ===\n", len(emptyDirs))
for _, d := range emptyDirs {
fmt.Fprintln(req.Stdout, d)
}
}
}
return nil
}
type fileEmptyOptions struct {
path string
filesOnly bool
dirsOnly bool
}
func parseFileEmptyArgs(args []string) (fileEmptyOptions, error) {
opts := fileEmptyOptions{path: "."}
var paths []string
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
paths = append(paths, args[i+1:]...)
i = len(args)
case arg == "--files-only":
opts.filesOnly = true
case arg == "--dirs-only":
opts.dirsOnly = true
case strings.HasPrefix(arg, "-") && arg != "-":
return opts, fmt.Errorf("未知选项 %q", arg)
default:
paths = append(paths, arg)
}
}
if len(paths) > 1 {
return opts, fmt.Errorf("一次只支持一个路径,收到 %d 个", len(paths))
}
if len(paths) == 1 {
opts.path = paths[0]
}
if opts.filesOnly && opts.dirsOnly {
return opts, fmt.Errorf("--files-only 和 --dirs-only 不能同时使用")
}
return opts, nil
}
+125
View File
@@ -0,0 +1,125 @@
package executor
import (
"bufio"
"context"
"fmt"
"os/exec"
"runtime"
"sort"
"strings"
)
// runNetConn implements `kopsc net conn`: show network connection statistics.
func runNetConn(ctx context.Context, req BuiltinRequest) error {
switch runtime.GOOS {
case "linux":
return netConnLinux(ctx, req)
case "darwin":
return netConnDarwin(ctx, req)
case "windows":
return netConnWindows(ctx, req)
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
func netConnLinux(ctx context.Context, req BuiltinRequest) error {
// Try ss first (faster)
if path, err := exec.LookPath("ss"); err == nil {
return netConnSS(ctx, path, req)
}
// Fallback to netstat
return netConnNetstat(ctx, req)
}
func netConnSS(ctx context.Context, path string, req BuiltinRequest) error {
out, err := exec.CommandContext(ctx, path, "-tan").Output()
if err != nil {
return fmt.Errorf("ss 执行失败: %w", err)
}
return parseNetstatOutput(string(out), req)
}
func netConnNetstat(ctx context.Context, req BuiltinRequest) error {
out, err := exec.CommandContext(ctx, "netstat", "-tan").Output()
if err != nil {
return fmt.Errorf("netstat 执行失败: %w", err)
}
return parseNetstatOutput(string(out), req)
}
func netConnDarwin(ctx context.Context, req BuiltinRequest) error {
out, err := exec.CommandContext(ctx, "netstat", "-tan").Output()
if err != nil {
return fmt.Errorf("netstat 执行失败: %w", err)
}
return parseNetstatOutput(string(out), req)
}
func netConnWindows(ctx context.Context, req BuiltinRequest) error {
out, err := exec.CommandContext(ctx, "netstat", "-an").Output()
if err != nil {
return fmt.Errorf("netstat 执行失败: %w", err)
}
return parseNetstatOutput(string(out), req)
}
func parseNetstatOutput(output string, req BuiltinRequest) error {
stats := make(map[string]int)
total := 0
scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
// Detect state column (usually last for TCP)
state := fields[len(fields)-1]
if isConnectionState(state) {
stats[state]++
total++
}
}
if total == 0 {
fmt.Fprintln(req.Stdout, "没有活动的 TCP 连接")
return nil
}
fmt.Fprintf(req.Stdout, "=== TCP 连接统计 (共 %d 个) ===\n\n", total)
// Sort by count descending
type stateCount struct {
state string
count int
}
var sorted []stateCount
for state, count := range stats {
sorted = append(sorted, stateCount{state, count})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].count > sorted[j].count
})
for _, sc := range sorted {
fmt.Fprintf(req.Stdout, "%-15s %d\n", sc.state, sc.count)
}
return nil
}
func isConnectionState(s string) bool {
switch s {
case "ESTABLISHED", "SYN_SENT", "SYN_RECV", "FIN_WAIT1", "FIN_WAIT2",
"TIME_WAIT", "CLOSE", "CLOSE_WAIT", "LAST_ACK", "LISTEN", "CLOSING":
return true
}
return false
}
+50
View File
@@ -0,0 +1,50 @@
package executor
import (
"context"
"fmt"
"net"
)
// runNetIface implements `kopsc net iface`: show network interfaces and IPs.
func runNetIface(ctx context.Context, req BuiltinRequest) error {
ifaces, err := net.Interfaces()
if err != nil {
return fmt.Errorf("获取网卡列表失败: %w", err)
}
for _, iface := range ifaces {
// Skip loopback if not requested
if iface.Flags&net.FlagLoopback != 0 {
continue
}
// Status
status := "DOWN"
if iface.Flags&net.FlagUp != 0 {
status = "UP"
}
fmt.Fprintf(req.Stdout, "%s: <%s> mtu %d\n", iface.Name, status, iface.MTU)
// MAC address
if len(iface.HardwareAddr) > 0 {
fmt.Fprintf(req.Stdout, " MAC: %s\n", iface.HardwareAddr.String())
}
// IP addresses
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
fmt.Fprintf(req.Stdout, " IP: %s\n", addr.String())
}
// Flags
fmt.Fprintf(req.Stdout, " 标志: %s\n", iface.Flags.String())
fmt.Fprintln(req.Stdout)
}
return nil
}
+168
View File
@@ -0,0 +1,168 @@
package executor
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// runNetSpeed implements `kopsc net speed`: simple network speed test.
func runNetSpeed(ctx context.Context, req BuiltinRequest) error {
opts, err := parseNetSpeedArgs(req.Args)
if err != nil {
return err
}
fmt.Fprint(req.Stdout, "=== 网络速度测试 ===\n")
// Download test
if !opts.uploadOnly {
fmt.Fprintln(req.Stdout, "测试下载速度...")
downloadSpeed, err := testDownload(ctx, opts.size)
if err != nil {
fmt.Fprintf(req.Stderr, "下载测试失败: %v\n", err)
} else {
fmt.Fprintf(req.Stdout, "下载速度: %s/s\n", humanSize(downloadSpeed))
}
fmt.Fprintln(req.Stdout)
}
// Upload test
if !opts.downloadOnly {
fmt.Fprintln(req.Stdout, "测试上传速度...")
uploadSpeed, err := testUpload(ctx, opts.size)
if err != nil {
fmt.Fprintf(req.Stderr, "上传测试失败: %v\n", err)
} else {
fmt.Fprintf(req.Stdout, "上传速度: %s/s\n", humanSize(uploadSpeed))
}
}
return nil
}
type netSpeedOptions struct {
size int64
downloadOnly bool
uploadOnly bool
}
func parseNetSpeedArgs(args []string) (netSpeedOptions, error) {
opts := netSpeedOptions{size: 10 * 1024 * 1024} // 10MB default
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
remaining := args[i+1:]
if len(remaining) > 0 {
n, err := parseSizeArg(remaining[0])
if err != nil {
return opts, fmt.Errorf("大小参数无效: %w", err)
}
opts.size = n
}
i = len(args)
case arg == "--size" || arg == "-s":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少大小参数", arg)
}
i++
n, err := parseSizeArg(args[i])
if err != nil {
return opts, fmt.Errorf("--size: %w", err)
}
opts.size = n
case strings.HasPrefix(arg, "--size="):
n, err := parseSizeArg(strings.TrimPrefix(arg, "--size="))
if err != nil {
return opts, fmt.Errorf("--size: %w", err)
}
opts.size = n
case arg == "--download" || arg == "-d":
opts.downloadOnly = true
case arg == "--upload" || arg == "-u":
opts.uploadOnly = true
default:
return opts, fmt.Errorf("未知选项 %q", arg)
}
}
if opts.downloadOnly && opts.uploadOnly {
return opts, fmt.Errorf("--download 和 --upload 不能同时使用")
}
return opts, nil
}
func testDownload(ctx context.Context, size int64) (int64, error) {
// Use Cloudflare's speed test endpoint
url := fmt.Sprintf("https://speed.cloudflare.com/__down?bytes=%d", size)
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return 0, err
}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
}
n, err := io.Copy(io.Discard, resp.Body)
if err != nil {
return 0, err
}
elapsed := time.Since(start).Seconds()
if elapsed == 0 {
elapsed = 0.001
}
return int64(float64(n) / elapsed), nil
}
func testUpload(ctx context.Context, size int64) (int64, error) {
// Use httpbin.org for upload test
url := "https://httpbin.org/post"
// Create a reader that generates data on the fly
data := make([]byte, size)
for i := range data {
data[i] = 'A'
}
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(data)))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/octet-stream")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
}
elapsed := time.Since(start).Seconds()
if elapsed == 0 {
elapsed = 0.001
}
return int64(float64(size) / elapsed), nil
}
+51
View File
@@ -0,0 +1,51 @@
package executor
import (
"context"
"fmt"
"os/exec"
"runtime"
)
// runProcTree implements `kopsc proc tree`: show process tree.
func runProcTree(ctx context.Context, req BuiltinRequest) error {
switch runtime.GOOS {
case "linux", "darwin":
return procTreeUnix(ctx, req)
case "windows":
return procTreeWindows(ctx, req)
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
func procTreeUnix(ctx context.Context, req BuiltinRequest) error {
// Try pstree first (best output)
if path, err := exec.LookPath("pstree"); err == nil {
cmd := exec.CommandContext(ctx, path, "-p")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
// Fallback: use ps with PPID and format a simple tree
// macOS doesn't support -H, so we use a different approach
if runtime.GOOS == "darwin" {
cmd := exec.CommandContext(ctx, "ps", "-eo", "pid,ppid,comm")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
// Linux fallback: ps with hierarchical output
cmd := exec.CommandContext(ctx, "ps", "-ejH")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
func procTreeWindows(ctx context.Context, req BuiltinRequest) error {
cmd := exec.CommandContext(ctx, "tasklist", "/tree")
cmd.Stdout = req.Stdout
cmd.Stderr = req.Stderr
return cmd.Run()
}
+133
View File
@@ -0,0 +1,133 @@
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)
}
+78
View File
@@ -0,0 +1,78 @@
package executor
import (
"context"
"fmt"
"os"
"os/exec"
"os/user"
"runtime"
"strings"
)
// runUserInfo implements `kopsc user info`: show user details.
func runUserInfo(ctx context.Context, req BuiltinRequest) error {
var username string
if len(req.Args) == 0 {
// Current user
u, err := user.Current()
if err != nil {
return fmt.Errorf("获取当前用户失败: %w", err)
}
username = u.Username
} else {
username = req.Args[0]
}
switch runtime.GOOS {
case "linux", "darwin":
return userInfoUnix(ctx, username, req)
case "windows":
return userInfoWindows(ctx, username, req)
default:
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
func userInfoUnix(ctx context.Context, username string, req BuiltinRequest) error {
// Use id command for comprehensive info
cmd := exec.CommandContext(ctx, "id", username)
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("用户 %q 不存在或 id 命令失败: %w", username, err)
}
fmt.Fprintf(req.Stdout, "用户: %s\n", strings.TrimSpace(string(out)))
// Get home directory and shell from /etc/passwd
if data, err := os.ReadFile("/etc/passwd"); err == nil {
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Split(line, ":")
if len(fields) >= 7 && fields[0] == username {
fmt.Fprintf(req.Stdout, "UID: %s\n", fields[2])
fmt.Fprintf(req.Stdout, "GID: %s\n", fields[3])
fmt.Fprintf(req.Stdout, "描述: %s\n", fields[4])
fmt.Fprintf(req.Stdout, "家目录: %s\n", fields[5])
fmt.Fprintf(req.Stdout, "Shell: %s\n", fields[6])
break
}
}
}
// Groups
groupsCmd := exec.CommandContext(ctx, "groups", username)
if groupsOut, err := groupsCmd.Output(); err == nil {
fmt.Fprintf(req.Stdout, "组: %s\n", strings.TrimSpace(string(groupsOut)))
}
return nil
}
func userInfoWindows(ctx context.Context, username string, req BuiltinRequest) error {
cmd := exec.CommandContext(ctx, "net", "user", username)
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("用户 %q 不存在或 net user 失败: %w", username, err)
}
fmt.Fprintln(req.Stdout, string(out))
return nil
}
+71
View File
@@ -836,6 +836,77 @@ func Builtin() *Registry {
RequiresYes: true, RequiresYes: true,
Variants: threePlatformBuiltins("fw.deny"), 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",
Group: "net",
Summary: "查看网卡信息和 IP 地址",
Action: "列出所有非回环网卡的信息:名称、状态、MAC 地址、IPv4/IPv6 地址、MTU。" +
"纯 Go 实现,跨平台一致",
Variants: threePlatformBuiltins("net.iface"),
},
{
ID: "net.conn",
Group: "net",
Summary: "查看 TCP 连接状态统计",
Action: "统计各状态的 TCP 连接数量(ESTABLISHED、TIME_WAIT、CLOSE_WAIT 等)。" +
"Linux: ss -s 或 netstat;macOS: netstat;Windows: netstat",
Variants: threePlatformBuiltins("net.conn"),
},
{
ID: "net.speed",
Group: "net",
Summary: "简单网速测试(下载/上传)",
Action: "从 Cloudflare 下载测试文件测量下载速度,向 httpbin.org 上传数据测量上传速度。" +
"默认测试 10MB,--size 可指定大小,--download 或 --upload 可单独测试",
Usage: "[--size 大小] [--download] [--upload]",
Variants: threePlatformBuiltins("net.speed"),
},
// ----- proc additions -----
{
ID: "proc.tree",
Group: "proc",
Summary: "查看进程树(父子关系)",
Action: "以树形结构显示进程的父子关系。" +
"Linux/macOS: pstree -p 或 ps -ejH;Windows: tasklist /tree",
Variants: threePlatformBuiltins("proc.tree"),
},
// ----- file additions -----
{
ID: "file.empty",
Group: "file",
Summary: "查找空文件和空目录",
Action: "递归扫描目录,找出大小为 0 的文件和没有任何内容的目录。" +
"--files-only 只查找空文件,--dirs-only 只查找空目录",
Usage: "[路径] [--files-only] [--dirs-only]",
Variants: threePlatformBuiltins("file.empty"),
},
// ----- user additions -----
{
ID: "user.info",
Group: "user",
Summary: "查看用户详细信息",
Action: "显示用户的 UID、GID、组、家目录、Shell 等信息。" +
"不指定用户名时显示当前用户。" +
"Linux/macOS: id + /etc/passwd + groups;Windows: net user",
Usage: "[用户名]",
Variants: threePlatformBuiltins("user.info"),
},
}} }}
} }
+5
View File
@@ -35,6 +35,11 @@ func TestBuiltinIDsStable(t *testing.T) {
"net.ping", "net.trace", "net.dns", "net.curl", "net.ping", "net.trace", "net.dns", "net.curl",
"log.search", "log.last", "log.search", "log.last",
"fw.status", "fw.allow", "fw.deny", "fw.status", "fw.allow", "fw.deny",
"sys.uptime",
"net.iface", "net.conn", "net.speed",
"proc.tree",
"file.empty",
"user.info",
} }
if got := reg.IDs(); !reflect.DeepEqual(got, want) { if got := reg.IDs(); !reflect.DeepEqual(got, want) {
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want) t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)