feat: 新增 file/svc/archive/sys/log/fw 分组并扩充 net、proc
- file:findbig 按大小、findrecent 按修改时间、findname 按名称查找文件 - svc:systemctl / launchctl / sc 统一接口,status、enable、disable、logs - archive:按扩展名自动识别格式并解压,支持 tar.gz/tgz/tar.bz2/tar.xz/tar/zip/gz - sysinfo:OS、CPU、内存、磁盘一览 - log:journalctl / log show / wevtutil 搜索关键字与查看最近日志 - fw:ufw / firewalld / iptables / ALF / netsh 统一接口,status、allow、deny - net 新增 ping、trace(mtr 优先)、dns(dig 优先)、curl HTTP 诊断 - proc 新增 topmem、topcpu、killname - 危险操作(svc enable/disable、proc killname、fw allow/deny)需 --yes 确认 - 注册表、executor 派发表、cli 分组说明与 README 同步更新
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# archive - 压缩文件
|
||||
|
||||
自动识别压缩格式并解压,不用记 `tar -xzf` / `tar -xjf` / `unzip` 的区别。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc archive extract <文件> [--dir 目录]` | 自动识别格式并解压 |
|
||||
|
||||
别名:`kopsc archive unzip`、`kopsc archive untar`
|
||||
|
||||
## 支持的格式
|
||||
|
||||
| 扩展名 | 解压方式 |
|
||||
| --- | --- |
|
||||
| `.tar.gz` / `.tgz` | Go 内置实现 |
|
||||
| `.tar.bz2` / `.tbz2` | 调用 `bzip2 -dc` + tar 解析 |
|
||||
| `.tar.xz` / `.txz` | 调用 `xz -dc` + tar 解析 |
|
||||
| `.tar` | Go 内置实现 |
|
||||
| `.zip` / `.jar` / `.war` | Go 内置实现 |
|
||||
| `.gz` | Go 内置 gzip 解压 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 解压到当前目录
|
||||
kopsc archive extract app.tar.gz
|
||||
|
||||
# 解压到指定目录
|
||||
kopsc archive extract backup.zip --dir /opt/backup
|
||||
|
||||
# 解压 tar.bz2
|
||||
kopsc archive extract data.tar.bz2
|
||||
```
|
||||
|
||||
## 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `--dir` / `-d` / `-C` | 目标目录(默认当前目录) |
|
||||
@@ -0,0 +1,53 @@
|
||||
# file - 文件查找
|
||||
|
||||
按大小、修改时间、名称查找文件,不用记 `find` 的复杂语法。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc file findbig [路径] [--threshold 大小]` | 查找大于指定阈值的文件,按大小排序 |
|
||||
| `kopsc file findrecent [路径] [--days 天数]` | 查找最近 N 天内修改过的文件 |
|
||||
| `kopsc file findname <模式> [路径]` | 按文件名查找(忽略大小写,支持 `*` `?` 通配符) |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 找当前目录下大于 100M 的文件(默认阈值)
|
||||
kopsc file findbig
|
||||
|
||||
# 找 /var 下大于 500M 的文件
|
||||
kopsc file findbig /var --threshold 500M
|
||||
|
||||
# 找最近 3 天修改过的文件
|
||||
kopsc file findrecent --days 3
|
||||
|
||||
# 找 /var/log 下所有 .log 文件
|
||||
kopsc file findname "*.log" /var/log
|
||||
|
||||
# 找所有 .conf 文件(忽略大小写)
|
||||
kopsc file findname "*.CONF"
|
||||
```
|
||||
|
||||
## 选项
|
||||
|
||||
### findbig
|
||||
|
||||
| 选项 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--threshold` / `-s` | `100M` | 大小阈值,支持 K/M/G/T 后缀 |
|
||||
| 位置参数 | `.` | 搜索路径 |
|
||||
|
||||
### findrecent
|
||||
|
||||
| 选项 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--days` / `-d` | `7` | 天数 |
|
||||
| 位置参数 | `.` | 搜索路径 |
|
||||
|
||||
### findname
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| 第一个位置参数 | 文件名模式(必填) |
|
||||
| 第二个位置参数 | 搜索路径(默认 `.`) |
|
||||
@@ -0,0 +1,45 @@
|
||||
# fw - 防火墙
|
||||
|
||||
统一管理 ufw / firewalld / iptables / Windows Firewall,不用记不同工具的命令。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc fw status` | 查看防火墙状态与规则 |
|
||||
| `kopsc fw allow <端口>[/<协议>]` | 开放端口 |
|
||||
| `kopsc fw deny <端口>[/<协议>]` | 关闭端口 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 查看防火墙状态
|
||||
kopsc fw status
|
||||
|
||||
# 开放 8080/tcp(默认协议)
|
||||
kopsc fw allow 8080 --yes
|
||||
|
||||
# 开放 53/udp
|
||||
kopsc fw allow 53/udp --yes
|
||||
|
||||
# 关闭 8080/tcp
|
||||
kopsc fw deny 8080 --yes
|
||||
|
||||
# 也可以写成 8080/tcp 形式
|
||||
kopsc fw allow 8080/tcp --yes
|
||||
```
|
||||
|
||||
## 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `-p` / `--protocol` | 协议(`tcp` 或 `udp`,默认 `tcp`) |
|
||||
| 端口格式 | `8080` 或 `8080/tcp` 或 `8080/udp` |
|
||||
|
||||
## 平台实现
|
||||
|
||||
| 操作 | Linux | macOS | Windows |
|
||||
| --- | --- | --- | --- |
|
||||
| status | ufw / firewalld / iptables(自动检测) | ALF(应用防火墙) | `netsh advfirewall` |
|
||||
| allow | `ufw allow` / `firewall-cmd --add-port` | 不支持 | `netsh advfirewall add rule` |
|
||||
| deny | `ufw deny` / `firewall-cmd --remove-port` | 不支持 | `netsh advfirewall delete rule` |
|
||||
@@ -0,0 +1,53 @@
|
||||
# log - 系统日志
|
||||
|
||||
在系统日志中搜索关键字或查看最近日志,不用记 `journalctl` / `log show` / `wevtutil` 的区别。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc log search <关键字> [选项]` | 在系统日志中搜索关键字 |
|
||||
| `kopsc log last [行数] [-u 服务]` | 查看最近的系统日志 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 搜索最近 1 小时包含 "error" 的日志
|
||||
kopsc log search "error" --since 1h
|
||||
|
||||
# 搜索 nginx 服务的日志
|
||||
kopsc log search "502" -u nginx
|
||||
|
||||
# 查看最近 50 行系统日志(默认)
|
||||
kopsc log last
|
||||
|
||||
# 查看最近 100 行
|
||||
kopsc log last 100
|
||||
|
||||
# 查看 nginx 服务的最近日志
|
||||
kopsc log last -u nginx
|
||||
```
|
||||
|
||||
## 选项
|
||||
|
||||
### log search
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `--since` | 时间范围,如 `1h`、`30m`、`2h30m` |
|
||||
| `-u` / `--unit` | 限定服务名 |
|
||||
| `-n` / `--lines` | 最大行数(默认 100) |
|
||||
|
||||
### log last
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `-n` / `--lines` | 行数(默认 50) |
|
||||
| `-u` / `--unit` | 限定服务名 |
|
||||
|
||||
## 平台实现
|
||||
|
||||
| 操作 | Linux | macOS | Windows |
|
||||
| --- | --- | --- | --- |
|
||||
| search | `journalctl --grep` | `log show --predicate` | `wevtutil` + 过滤 |
|
||||
| last | `journalctl -n` | `log show --last` | `wevtutil /c:` |
|
||||
@@ -143,14 +143,20 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
||||
| 分组 | 用途 | 文档 |
|
||||
| --- | --- | --- |
|
||||
| **docker** | 容器与镜像:查看、启停、日志、清理、导出/导入、Compose | [README.docker.md](README.docker.md) |
|
||||
| **net** | 网络与端口:端口被谁占用、本机监听了哪些端口 | [README.net.md](README.net.md) |
|
||||
| **net** | 网络与端口:端口被谁占用、监听端口、ping、DNS、路由追踪、HTTP 诊断 | [README.net.md](README.net.md) |
|
||||
| **disk** | 磁盘与文件占用:哪个目录、哪个文件最占空间 | [README.disk.md](README.disk.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) |
|
||||
| **git** | Git 历史:按日期范围调整提交时间并强推 | [README.git.md](README.git.md) |
|
||||
| **pkg** | 系统软件包:刷新包管理器索引 | [README.pkg.md](README.pkg.md) |
|
||||
| **user** | 用户管理:授予或移除用户的 root/sudo 权限 | [README.user.md](README.user.md) |
|
||||
| **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) |
|
||||
| **log** | 系统日志:搜索关键字、查看最近日志 | [README.log.md](README.log.md) |
|
||||
| **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) |
|
||||
|
||||
辅助命令(不涉及具体资源,直接用即可):
|
||||
|
||||
@@ -175,11 +181,29 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
||||
| 排障 | `kopsc diagnose web` |
|
||||
| 后台跑服务 | `kopsc proc nohup ./server` |
|
||||
| 等进程退出 | `kopsc proc wait nginx`、`kopsc proc wait --pid 1234 --timeout 30` |
|
||||
| 看进程资源排行 | `kopsc proc topmem`、`kopsc proc topcpu` |
|
||||
| 按名字杀进程 | `kopsc proc killname nginx --yes` |
|
||||
| 跑 Java 应用 | `kopsc java run app.jar`、`kopsc java run --jdk 17 --print-flags app.jar` |
|
||||
| 调整提交时间 | `kopsc git retime 2026-10-01 2026-10-15`(加 `--yes` 才执行) |
|
||||
| 刷新软件源 | `kopsc pkg refreshindex` |
|
||||
| 授予用户 root 权限 | `kopsc user grant alice --yes` |
|
||||
| 移除用户 root 权限 | `kopsc user revoke alice --yes` |
|
||||
| 找大文件 | `kopsc file findbig /var --threshold 500M` |
|
||||
| 找最近修改的文件 | `kopsc file findrecent /home --days 3` |
|
||||
| 按名字找文件 | `kopsc file findname "*.log" /var` |
|
||||
| 看服务状态 | `kopsc svc status nginx` |
|
||||
| 启用/禁用服务 | `kopsc svc enable nginx --yes` / `kopsc svc disable nginx --yes` |
|
||||
| 看服务日志 | `kopsc svc logs nginx -f` |
|
||||
| 解压文件 | `kopsc archive extract app.tar.gz` |
|
||||
| 看系统信息 | `kopsc sysinfo` |
|
||||
| 测试连通性 | `kopsc net ping example.com` |
|
||||
| 路由追踪 | `kopsc net trace example.com` |
|
||||
| DNS 查询 | `kopsc net dns example.com -t MX` |
|
||||
| HTTP 诊断 | `kopsc net curl https://example.com -L` |
|
||||
| 搜索系统日志 | `kopsc log search "error" --since 1h` |
|
||||
| 看最近日志 | `kopsc log last 100` |
|
||||
| 看防火墙状态 | `kopsc fw status` |
|
||||
| 开放/关闭端口 | `kopsc fw allow 8080/tcp --yes` / `kopsc fw deny 8080/tcp --yes` |
|
||||
|
||||
## 平台识别
|
||||
|
||||
@@ -243,6 +267,12 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
||||
| java | [`README.java.md`](README.java.md) |
|
||||
| git | [`README.git.md`](README.git.md) |
|
||||
| user | [`README.user.md`](README.user.md) |
|
||||
| file | [`README.file.md`](README.file.md) |
|
||||
| svc | [`README.svc.md`](README.svc.md) |
|
||||
| archive | [`README.archive.md`](README.archive.md) |
|
||||
| sys | [`README.sys.md`](README.sys.md) |
|
||||
| log | [`README.log.md`](README.log.md) |
|
||||
| fw | [`README.fw.md`](README.fw.md) |
|
||||
|
||||
新分组需要新建 `README.<分组>.md`,并在本文件「命令一览」的表格里加一行链接。
|
||||
每条命令至少要写清:**作用**、**实际执行什么**、**示例**;有独有选项时补选项表。
|
||||
|
||||
+70
-1
@@ -2,11 +2,15 @@
|
||||
|
||||
> 返回 [README.md](README.md)
|
||||
|
||||
查看端口被谁占用、本机监听了哪些端口。两个命令都只读,不需要 `--yes`。
|
||||
查看端口被谁占用、本机监听了哪些端口、测试连通性、DNS 查询、路由追踪、HTTP 诊断。
|
||||
|
||||
```bash
|
||||
kopsc net pids 8080 # 谁占用了 8080 端口(输出 PID)
|
||||
kopsc net ports # 本机正在监听哪些端口(统一表格)
|
||||
kopsc net ping example.com # 测试连通性
|
||||
kopsc net trace example.com # 路由追踪
|
||||
kopsc net dns example.com # DNS 查询
|
||||
kopsc net curl https://example.com # HTTP 诊断
|
||||
```
|
||||
|
||||
## 端口占用查询(net.pids)
|
||||
@@ -54,3 +58,68 @@ $ kopsc net ports
|
||||
- 通配地址 `0.0.0.0` 与 `::` 统一显示为 `*`。
|
||||
- `--raw` 保留平台原生命令的原始输出,供依赖原生格式的脚本使用。
|
||||
- 没有监听端口时 stdout 为空,stderr 提示「没有正在监听的端口」。
|
||||
|
||||
## 连通性测试(net.ping)
|
||||
|
||||
```bash
|
||||
kopsc net ping example.com # 默认 4 个包
|
||||
kopsc net ping example.com -c 10 # 发送 10 个包
|
||||
```
|
||||
|
||||
底层调用系统 `ping` 命令。
|
||||
|
||||
## 路由追踪(net.trace)
|
||||
|
||||
```bash
|
||||
kopsc net trace example.com
|
||||
```
|
||||
|
||||
优先使用 `mtr --report`(最佳输出),回退到 `traceroute`(Linux/macOS)或 `tracert`(Windows)。
|
||||
|
||||
## DNS 查询(net.dns)
|
||||
|
||||
```bash
|
||||
kopsc net dns example.com # 默认 A 记录
|
||||
kopsc net dns example.com -t MX # 查询 MX 记录
|
||||
kopsc net dns example.com -t AAAA # 查询 AAAA 记录
|
||||
kopsc net dns example.com -s 8.8.8.8 # 指定 DNS 服务器
|
||||
```
|
||||
|
||||
优先使用 `dig`,回退到 `nslookup` 或 `host`。
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `-t` / `--type` | 记录类型(A/AAAA/MX/CNAME 等) |
|
||||
| `-s` / `--server` | DNS 服务器地址 |
|
||||
|
||||
## HTTP 诊断(net.curl)
|
||||
|
||||
```bash
|
||||
kopsc net curl https://example.com # 基本诊断
|
||||
kopsc net curl https://example.com -L # 跟随重定向
|
||||
kopsc net curl https://api.example.com -X POST -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
HTTP 状态码: 200
|
||||
重定向次数: 0
|
||||
DNS 解析: 0.005s
|
||||
TCP 连接: 0.032s
|
||||
TLS 握手: 0.089s
|
||||
首字节: 0.145s
|
||||
总耗时: 0.146s
|
||||
下载大小: 1234 bytes
|
||||
下载速度: 8452.05 bytes/s
|
||||
```
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `-L` / `--follow` | 跟随重定向 |
|
||||
| `-X` / `--method` | HTTP 方法(GET/POST/PUT 等) |
|
||||
| `-H` / `--header` | 添加请求头,可重复 |
|
||||
|
||||
+43
-1
@@ -2,11 +2,15 @@
|
||||
|
||||
> 返回 [README.md](README.md)
|
||||
|
||||
在后台运行命令并写日志,关掉终端也不会退出;也可以等某个进程退出后再继续。
|
||||
在后台运行命令并写日志,关掉终端也不会退出;也可以等某个进程退出后再继续;
|
||||
还能按资源占用排序查看进程、按名称终止进程。
|
||||
|
||||
```bash
|
||||
kopsc proc nohup ./server # 后台运行命令,输出写日志
|
||||
kopsc proc wait nginx # 等 nginx 退出后返回
|
||||
kopsc proc topmem # 内存占用前 10 的进程
|
||||
kopsc proc topcpu # CPU 占用前 10 的进程
|
||||
kopsc proc killname nginx --yes # 终止名为 nginx 的进程
|
||||
```
|
||||
|
||||
## 后台运行(proc.nohup)
|
||||
@@ -86,3 +90,41 @@ kopsc proc wait nginx --interval 500ms -q # 每 0.5 秒查一次,不打印
|
||||
|
||||
> 关键字请直接作为参数书写;若关键字以 `-` 开头,用 `--` 分隔,例如 `kopsc proc wait -- -weird`。
|
||||
> `--dry-run` 只打印判定依据,不会去查询进程。
|
||||
|
||||
## 按资源排序查看进程(proc.topmem / proc.topcpu)
|
||||
|
||||
```bash
|
||||
kopsc proc topmem # 内存占用前 10
|
||||
kopsc proc topcpu # CPU 占用前 10
|
||||
kopsc proc topmem 20 # 内存占用前 20
|
||||
kopsc proc topcpu -n 5 # CPU 占用前 5
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
=== 占用 CPU 前 10 的进程 ===
|
||||
|
||||
PID CPU 进程 命令
|
||||
1234 45.2% java java -jar app.jar --spring.profiles.active=prod
|
||||
5678 12.3% nginx nginx: worker process
|
||||
```
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
| --- | --- |
|
||||
| `-n` / `--count` / 位置参数 | 显示数量(默认 10) |
|
||||
|
||||
> Windows 下 `proc topcpu` 不可用(tasklist 不报告 CPU 使用率)。
|
||||
|
||||
## 按进程名终止进程(proc.killname)
|
||||
|
||||
```bash
|
||||
kopsc proc killname nginx # 列出匹配进程(不执行)
|
||||
kopsc proc killname nginx --yes # 确认终止
|
||||
```
|
||||
|
||||
匹配规则:进程名或命令行中包含指定关键字(忽略大小写)。
|
||||
|
||||
> 这是破坏性命令,未加 `--yes` 时只列出匹配进程并以退出码 2 结束。
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# svc - 服务管理
|
||||
|
||||
统一管理 systemd / launchctl / sc 的服务操作,不用记不同平台的命令。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc svc status <服务名>` | 查看服务状态与最近 20 行日志 |
|
||||
| `kopsc svc enable <服务名>` | 启用并启动服务(开机自启 + 立即启动) |
|
||||
| `kopsc svc disable <服务名>` | 禁用并停止服务 |
|
||||
| `kopsc svc logs <服务名> [-f] [-n 行数]` | 查看服务日志,`-f` 实时跟踪 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 查看 nginx 状态和最近日志
|
||||
kopsc svc status nginx
|
||||
|
||||
# 启用并启动 nginx
|
||||
kopsc svc enable nginx --yes
|
||||
|
||||
# 禁用并停止 nginx
|
||||
kopsc svc disable nginx --yes
|
||||
|
||||
# 查看 nginx 最近 100 行日志
|
||||
kopsc svc logs nginx -n 100
|
||||
|
||||
# 实时跟踪 nginx 日志
|
||||
kopsc svc logs nginx -f
|
||||
```
|
||||
|
||||
## 平台实现
|
||||
|
||||
| 操作 | Linux | macOS | Windows |
|
||||
| --- | --- | --- | --- |
|
||||
| status | `systemctl status` + `journalctl` | `launchctl list` + `log show` | `sc query` |
|
||||
| enable | `systemctl enable` + `start` | `launchctl load -w` | `sc config start=auto` + `start` |
|
||||
| disable | `systemctl stop` + `disable` | `launchctl unload -w` | `sc stop` + `config start=disabled` |
|
||||
| logs | `journalctl -u` | `log show` | `wevtutil` |
|
||||
@@ -0,0 +1,51 @@
|
||||
# sys - 系统信息
|
||||
|
||||
一次性查看 OS、CPU、内存、磁盘概况,不用分别跑 `uname`、`lscpu`、`free`、`df`。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `kopsc sysinfo` | 显示系统信息一览 |
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
kopsc sysinfo
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
=== 系统信息 ===
|
||||
|
||||
操作系统: linux/amd64
|
||||
发行版: centos
|
||||
主机名: web-01
|
||||
内核: 5.15.0-91-generic
|
||||
|
||||
=== CPU ===
|
||||
|
||||
逻辑核心: 4
|
||||
型号: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz
|
||||
|
||||
=== 内存 ===
|
||||
|
||||
总计: 16.0G
|
||||
检测方式: /proc/meminfo
|
||||
可用: 8.2G
|
||||
|
||||
=== 磁盘 ===
|
||||
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/vda1 50G 22G 28G 44% /
|
||||
```
|
||||
|
||||
## 平台实现
|
||||
|
||||
| 信息 | Linux | macOS | Windows |
|
||||
| --- | --- | --- | --- |
|
||||
| OS | `/etc/os-release` | runtime.GOOS | runtime.GOOS |
|
||||
| CPU | `/proc/cpuinfo` | `sysctl` | runtime |
|
||||
| 内存 | `/proc/meminfo` + cgroup | `sysctl hw.memsize` | `wmic` |
|
||||
| 磁盘 | `df -h` | `df -h` | `wmic logicaldisk` |
|
||||
+8
-2
@@ -218,12 +218,18 @@ func translateHelp(text string) string {
|
||||
var groupDescriptions = map[string]string{
|
||||
"docker": "Docker 容器与镜像管理(查看、启停、日志、清理、导出/导入、Compose)",
|
||||
"disk": "磁盘与文件占用:查看哪个目录、哪个文件最占空间",
|
||||
"net": "网络与端口:端口被谁占用、本机监听了哪些端口",
|
||||
"net": "网络与端口:端口被谁占用、本机监听了哪些端口、连通性、DNS、路由追踪、HTTP 诊断",
|
||||
"pkg": "系统软件包:刷新包管理器(dnf/apt/apk/brew/winget)的软件源索引",
|
||||
"proc": "本机进程:在后台运行命令并写日志,关掉终端也不退出",
|
||||
"proc": "本机进程:后台运行命令并写日志、等待进程退出、按资源排序、按名称终止",
|
||||
"java": "Java 应用:按 JDK 版本自动套用调优参数并前台运行 jar",
|
||||
"git": "Git 历史:把指定日期范围的提交时间改到 21:00-23:00 并强推",
|
||||
"user": "用户管理:授予或移除用户的 root/sudo 权限",
|
||||
"file": "文件查找:按大小、修改时间、名称查找文件",
|
||||
"svc": "服务管理:查看状态、启用/禁用、查看日志(systemctl/launchctl/sc 统一接口)",
|
||||
"archive": "压缩文件:自动识别格式并解压",
|
||||
"sys": "系统信息:OS、CPU、内存、磁盘一览",
|
||||
"log": "系统日志:搜索关键字、查看最近日志",
|
||||
"fw": "防火墙:查看状态、开放/关闭端口(ufw/firewalld/iptables 统一接口)",
|
||||
}
|
||||
|
||||
// newGroup builds an intermediate command such as `kopsc disk`.
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runArchiveExtract implements `kopsc archive extract <file>`: auto-detect
|
||||
// archive format and extract to current directory (or --dir).
|
||||
func runArchiveExtract(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseArchiveExtractArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src := opts.file
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return fmt.Errorf("文件 %q 不存在: %w", src, err)
|
||||
}
|
||||
|
||||
dest := opts.dest
|
||||
if dest == "" {
|
||||
dest = "."
|
||||
}
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return fmt.Errorf("创建目标目录 %q 失败: %w", dest, err)
|
||||
}
|
||||
|
||||
lower := strings.ToLower(src)
|
||||
switch {
|
||||
case strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz"):
|
||||
return extractTarGz(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar.bz2") || strings.HasSuffix(lower, ".tbz2"):
|
||||
return extractTarViaPipe(ctx, "bzip2", "-dc", src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar.xz") || strings.HasSuffix(lower, ".txz"):
|
||||
return extractTarViaPipe(ctx, "xz", "-dc", src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar"):
|
||||
return extractTarFile(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".jar") || strings.HasSuffix(lower, ".war"):
|
||||
return extractZip(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".gz"):
|
||||
return extractGz(src, dest, req)
|
||||
default:
|
||||
return fmt.Errorf("无法识别的压缩格式: %q(支持 .tar.gz .tgz .tar.bz2 .tar.xz .tar .zip .gz)", src)
|
||||
}
|
||||
}
|
||||
|
||||
type archiveExtractOptions struct {
|
||||
file string
|
||||
dest string
|
||||
}
|
||||
|
||||
func parseArchiveExtractArgs(args []string) (archiveExtractOptions, error) {
|
||||
var opts archiveExtractOptions
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "--dir" || arg == "-d" || arg == "-C":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少目标目录", arg)
|
||||
}
|
||||
i++
|
||||
opts.dest = args[i]
|
||||
case strings.HasPrefix(arg, "--dir="):
|
||||
opts.dest = strings.TrimPrefix(arg, "--dir=")
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少压缩文件路径;用法: kopsc archive extract <文件>")
|
||||
}
|
||||
opts.file = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func extractTarGz(src, dest string, req BuiltinRequest) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 gzip 失败: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
fmt.Fprintf(req.Stdout, "解压 tar.gz: %s -> %s\n", src, dest)
|
||||
return writeTar(tar.NewReader(gz), dest, req)
|
||||
}
|
||||
|
||||
func extractTarFile(src, dest string, req BuiltinRequest) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(req.Stdout, "解压 tar: %s -> %s\n", src, dest)
|
||||
return writeTar(tar.NewReader(f), dest, req)
|
||||
}
|
||||
|
||||
func extractTarViaPipe(ctx context.Context, decompressor, flag, src, dest string, req BuiltinRequest) error {
|
||||
if _, err := exec.LookPath(decompressor); err != nil {
|
||||
return fmt.Errorf("解压 %s 需要 %s,请先安装", filepath.Ext(src), decompressor)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "解压 %s: %s -> %s\n", decompressor, src, dest)
|
||||
cmd := exec.CommandContext(ctx, decompressor, flag, src)
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stderr = req.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("启动 %s 失败: %w", decompressor, err)
|
||||
}
|
||||
err = writeTar(tar.NewReader(stdoutPipe), dest, req)
|
||||
if waitErr := cmd.Wait(); waitErr != nil && err == nil {
|
||||
err = waitErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func writeTar(tr *tar.Reader, dest string, req BuiltinRequest) error {
|
||||
count := 0
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 tar 失败: %w", err)
|
||||
}
|
||||
target := filepath.Join(dest, header.Name)
|
||||
if !isWithinDir(target, dest) {
|
||||
return fmt.Errorf("tar 条目 %q 试图逃逸出目标目录", header.Name)
|
||||
}
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
count++
|
||||
case tar.TypeSymlink:
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
os.Remove(target)
|
||||
if err := os.Symlink(header.Linkname, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压 %d 个文件\n", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZip(src, dest string, req BuiltinRequest) error {
|
||||
fmt.Fprintf(req.Stdout, "解压 zip: %s -> %s\n", src, dest)
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 zip 失败: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
count := 0
|
||||
for _, f := range r.File {
|
||||
target := filepath.Join(dest, f.Name)
|
||||
if !isWithinDir(target, dest) {
|
||||
return fmt.Errorf("zip 条目 %q 试图逃逸出目标目录", f.Name)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(out, rc)
|
||||
rc.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count++
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压 %d 个文件\n", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractGz(src, dest string, req BuiltinRequest) error {
|
||||
fmt.Fprintf(req.Stdout, "解压 gz: %s -> %s\n", src, dest)
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 gzip 失败: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
outName := strings.TrimSuffix(filepath.Base(src), ".gz")
|
||||
if outName == "" {
|
||||
outName = "output"
|
||||
}
|
||||
target := filepath.Join(dest, outName)
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, gz); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压: %s\n", target)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWithinDir checks that target is inside dest (prevents zip-slip attacks).
|
||||
func isWithinDir(target, dest string) bool {
|
||||
rel, err := filepath.Rel(dest, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".."
|
||||
}
|
||||
@@ -32,6 +32,7 @@ type BuiltinRequest struct {
|
||||
type Builtin func(ctx context.Context, req BuiltinRequest) error
|
||||
|
||||
var builtins = map[string]Builtin{
|
||||
"archive.extract": runArchiveExtract,
|
||||
"disk.usage": runDiskUsage,
|
||||
"diagnose": runDiagnose,
|
||||
"docker.df": runDockerDF,
|
||||
@@ -46,14 +47,34 @@ var builtins = map[string]Builtin{
|
||||
"docker.save": runDockerSave,
|
||||
"docker.stats": runDockerStats,
|
||||
"docker.stop": runDockerStop,
|
||||
"file.findbig": runFileFindBig,
|
||||
"file.findname": runFileFindName,
|
||||
"file.findrecent": runFileFindRecent,
|
||||
"fw.allow": runFwAllow,
|
||||
"fw.deny": runFwDeny,
|
||||
"fw.status": runFwStatus,
|
||||
"git.retime": runGitRetime,
|
||||
"git.retime.preview": runGitRetimePreview,
|
||||
"java.run": runJavaRun,
|
||||
"java.run.preview": runJavaPreview,
|
||||
"log.last": runLogLast,
|
||||
"log.search": runLogSearch,
|
||||
"net.curl": runNetCurl,
|
||||
"net.dns": runNetDNS,
|
||||
"net.ping": runNetPing,
|
||||
"net.pids": runNetPIDs,
|
||||
"net.ports": runNetPorts,
|
||||
"net.trace": runNetTrace,
|
||||
"proc.killname": runProcKillName,
|
||||
"proc.nohup": runNohup,
|
||||
"proc.topcpu": runProcTopCPU,
|
||||
"proc.topmem": runProcTopMem,
|
||||
"proc.wait": runProcWait,
|
||||
"svc.disable": runSvcDisable,
|
||||
"svc.enable": runSvcEnable,
|
||||
"svc.logs": runSvcLogs,
|
||||
"svc.status": runSvcStatus,
|
||||
"sysinfo": runSysInfo,
|
||||
"user.grant": runUserGrant,
|
||||
"user.revoke": runUserRevoke,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runFileFindBig implements `kopsc file findbig`: find files larger than a
|
||||
// threshold, sorted by size descending.
|
||||
func runFileFindBig(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFileFindBigArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := filepath.Abs(opts.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法解析路径 %q: %w", opts.path, err)
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
path string
|
||||
size int64
|
||||
}
|
||||
var results []fileEntry
|
||||
|
||||
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() {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
if info.Size() >= opts.threshold {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
if rel == "" {
|
||||
rel = path
|
||||
}
|
||||
results = append(results, fileEntry{path: rel, size: info.Size()})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].size > results[j].size
|
||||
})
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Fprintf(req.Stdout, "没有找到大于 %s 的文件\n", humanSize(opts.threshold))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range results {
|
||||
fmt.Fprintf(req.Stdout, "%-8s %s\n", humanSize(f.size), f.path)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "\n共 %d 个文件大于 %s\n", len(results), humanSize(opts.threshold))
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileFindBigOptions struct {
|
||||
path string
|
||||
threshold int64
|
||||
}
|
||||
|
||||
func parseFileFindBigArgs(args []string) (fileFindBigOptions, error) {
|
||||
opts := fileFindBigOptions{path: ".", threshold: 100 * 1024 * 1024}
|
||||
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 == "--threshold" || arg == "-s":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少大小参数,例如 --threshold 50M", arg)
|
||||
}
|
||||
i++
|
||||
n, err := parseSizeArg(args[i])
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("%s: %w", arg, err)
|
||||
}
|
||||
opts.threshold = n
|
||||
case strings.HasPrefix(arg, "--threshold="):
|
||||
n, err := parseSizeArg(strings.TrimPrefix(arg, "--threshold="))
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("--threshold: %w", err)
|
||||
}
|
||||
opts.threshold = n
|
||||
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]
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// parseSizeArg parses human-friendly size strings: "100M", "1G", "500K", "1024".
|
||||
func parseSizeArg(s string) (int64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("缺少大小值")
|
||||
}
|
||||
multiplier := int64(1)
|
||||
upper := strings.ToUpper(s)
|
||||
if strings.HasSuffix(upper, "TB") || strings.HasSuffix(upper, "T") {
|
||||
multiplier = 1024 * 1024 * 1024 * 1024
|
||||
} else if strings.HasSuffix(upper, "GB") || strings.HasSuffix(upper, "G") {
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
} else if strings.HasSuffix(upper, "MB") || strings.HasSuffix(upper, "M") {
|
||||
multiplier = 1024 * 1024
|
||||
} else if strings.HasSuffix(upper, "KB") || strings.HasSuffix(upper, "K") {
|
||||
multiplier = 1024
|
||||
} else if strings.HasSuffix(upper, "B") {
|
||||
multiplier = 1
|
||||
}
|
||||
|
||||
numStr := s
|
||||
if multiplier > 1 {
|
||||
numStr = s[:len(s)-lenSuffix(s)]
|
||||
}
|
||||
numStr = strings.TrimSpace(numStr)
|
||||
|
||||
n, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无法解析大小 %q", s)
|
||||
}
|
||||
return int64(n * float64(multiplier)), nil
|
||||
}
|
||||
|
||||
func lenSuffix(s string) int {
|
||||
i := len(s)
|
||||
for i > 0 && !isDigit(s[i-1]) && s[i-1] != '.' {
|
||||
i--
|
||||
}
|
||||
return len(s) - i
|
||||
}
|
||||
|
||||
func isDigit(b byte) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
|
||||
// runFileFindRecent implements `kopsc file findrecent`: find files modified
|
||||
// within the last N days.
|
||||
func runFileFindRecent(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFileFindRecentArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := filepath.Abs(opts.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法解析路径 %q: %w", opts.path, err)
|
||||
}
|
||||
|
||||
cutoff := time.Now().AddDate(0, 0, -opts.days)
|
||||
type fileEntry struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
}
|
||||
var results []fileEntry
|
||||
|
||||
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() {
|
||||
return nil
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
if info.ModTime().After(cutoff) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
if rel == "" {
|
||||
rel = path
|
||||
}
|
||||
results = append(results, fileEntry{path: rel, modTime: info.ModTime()})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].modTime.After(results[j].modTime)
|
||||
})
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Fprintf(req.Stdout, "最近 %d 天内没有修改过的文件\n", opts.days)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range results {
|
||||
fmt.Fprintf(req.Stdout, "%s %s\n", f.modTime.Format("2006-01-02 15:04"), f.path)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "\n共 %d 个文件在最近 %d 天内修改\n", len(results), opts.days)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileFindRecentOptions struct {
|
||||
path string
|
||||
days int
|
||||
}
|
||||
|
||||
func parseFileFindRecentArgs(args []string) (fileFindRecentOptions, error) {
|
||||
opts := fileFindRecentOptions{path: ".", days: 7}
|
||||
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 == "--days" || arg == "-d":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少天数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--days 需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.days = n
|
||||
case strings.HasPrefix(arg, "--days="):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--days="))
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--days 需要正整数,收到 %q", strings.TrimPrefix(arg, "--days="))
|
||||
}
|
||||
opts.days = n
|
||||
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]
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// runFileFindName implements `kopsc file findname`: find files by name pattern
|
||||
// (case-insensitive glob match).
|
||||
func runFileFindName(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFileFindNameArgs(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 results []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() {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if matchGlob(opts.pattern, name) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
if rel == "" {
|
||||
rel = path
|
||||
}
|
||||
results = append(results, rel)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(results)
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Fprintf(req.Stdout, "没有找到匹配 %q 的文件\n", opts.pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range results {
|
||||
fmt.Fprintln(req.Stdout, f)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "\n共 %d 个文件匹配\n", len(results))
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileFindNameOptions struct {
|
||||
path string
|
||||
pattern string
|
||||
}
|
||||
|
||||
func parseFileFindNameArgs(args []string) (fileFindNameOptions, error) {
|
||||
opts := fileFindNameOptions{path: "."}
|
||||
var paths []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
remaining := args[i+1:]
|
||||
if len(remaining) > 0 && opts.pattern == "" {
|
||||
opts.pattern = remaining[0]
|
||||
paths = append(paths, remaining[1:]...)
|
||||
} else {
|
||||
paths = append(paths, remaining...)
|
||||
}
|
||||
i = len(args)
|
||||
case arg == "--pattern" || arg == "-p":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少模式参数", arg)
|
||||
}
|
||||
i++
|
||||
opts.pattern = args[i]
|
||||
case strings.HasPrefix(arg, "--pattern="):
|
||||
opts.pattern = strings.TrimPrefix(arg, "--pattern=")
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
if opts.pattern == "" {
|
||||
opts.pattern = arg
|
||||
} else {
|
||||
paths = append(paths, arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if opts.pattern == "" {
|
||||
return opts, fmt.Errorf("必须提供文件名模式,例如 kopsc file findname \"*.log\"")
|
||||
}
|
||||
if len(paths) > 1 {
|
||||
return opts, fmt.Errorf("一次只支持一个路径,收到 %d 个", len(paths))
|
||||
}
|
||||
if len(paths) == 1 {
|
||||
opts.path = paths[0]
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// matchGlob does a case-insensitive filepath.Match.
|
||||
func matchGlob(pattern, name string) bool {
|
||||
if ok, _ := filepath.Match(strings.ToLower(pattern), strings.ToLower(name)); ok {
|
||||
return true
|
||||
}
|
||||
if ok, _ := filepath.Match(pattern, name); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runFwStatus implements `kopsc fw status`: show current firewall rules.
|
||||
func runFwStatus(ctx context.Context, req BuiltinRequest) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return fwStatusLinux(ctx, req)
|
||||
case "darwin":
|
||||
return fwStatusDarwin(ctx, req)
|
||||
case "windows":
|
||||
return fwStatusWindows(ctx, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func fwStatusLinux(ctx context.Context, req BuiltinRequest) error {
|
||||
// Try ufw first, then firewalld, then iptables.
|
||||
if path, err := exec.LookPath("ufw"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, path, "status", "verbose")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
if path, err := exec.LookPath("firewall-cmd"); err == nil {
|
||||
fmt.Fprintln(req.Stdout, "=== firewalld 状态 ===")
|
||||
cmd := exec.CommandContext(ctx, path, "--state")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
_ = cmd.Run()
|
||||
|
||||
fmt.Fprintln(req.Stdout, "\n=== 已开放端口 ===")
|
||||
listCmd := exec.CommandContext(ctx, path, "--list-ports")
|
||||
listCmd.Stdout = req.Stdout
|
||||
listCmd.Stderr = req.Stderr
|
||||
_ = listCmd.Run()
|
||||
|
||||
fmt.Fprintln(req.Stdout, "\n=== 已允许服务 ===")
|
||||
svcCmd := exec.CommandContext(ctx, path, "--list-services")
|
||||
svcCmd.Stdout = req.Stdout
|
||||
svcCmd.Stderr = req.Stderr
|
||||
return svcCmd.Run()
|
||||
}
|
||||
if path, err := exec.LookPath("iptables"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, path, "-L", "-n", "--line-numbers")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
return fmt.Errorf("未找到 ufw、firewall-cmd 或 iptables")
|
||||
}
|
||||
|
||||
func fwStatusDarwin(ctx context.Context, req BuiltinRequest) error {
|
||||
fmt.Fprintln(req.Stdout, "macOS 使用内置应用防火墙(ALF)")
|
||||
cmd := exec.CommandContext(ctx, "/usr/libexec/ApplicationFirewall/socketfilterfw", "--getglobalstate")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
_ = cmd.Run()
|
||||
|
||||
fmt.Fprintln(req.Stdout, "\n=== 已允许的应用 ===")
|
||||
listCmd := exec.CommandContext(ctx, "/usr/libexec/ApplicationFirewall/socketfilterfw", "--listapps")
|
||||
listCmd.Stdout = req.Stdout
|
||||
listCmd.Stderr = req.Stderr
|
||||
return listCmd.Run()
|
||||
}
|
||||
|
||||
func fwStatusWindows(ctx context.Context, req BuiltinRequest) error {
|
||||
cmd := exec.CommandContext(ctx, "netsh", "advfirewall", "firewall", "show", "rule", "name=all", "dir=in", "status=enabled")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// runFwAllow implements `kopsc fw allow <port>/<protocol>`: open a port.
|
||||
func runFwAllow(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFwPortArgs(req.Args, "allow")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return fwAllowLinux(ctx, opts, req)
|
||||
case "darwin":
|
||||
return fmt.Errorf("macOS 不支持通过命令行开放端口,请在系统偏好设置中配置")
|
||||
case "windows":
|
||||
return fwAllowWindows(ctx, opts, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func fwAllowLinux(ctx context.Context, opts fwPortOptions, req BuiltinRequest) error {
|
||||
portProto := fmt.Sprintf("%d/%s", opts.port, opts.protocol)
|
||||
if path, err := exec.LookPath("ufw"); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "开放端口: %s (ufw)\n", portProto)
|
||||
return runCommand(ctx, path, []string{"allow", portProto}, req)
|
||||
}
|
||||
if path, err := exec.LookPath("firewall-cmd"); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "开放端口: %s (firewalld)\n", portProto)
|
||||
if err := runCommand(ctx, path, []string{"--add-port=" + portProto, "--permanent"}, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCommand(ctx, path, []string{"--reload"}, req)
|
||||
}
|
||||
return fmt.Errorf("未找到 ufw 或 firewall-cmd")
|
||||
}
|
||||
|
||||
func fwAllowWindows(ctx context.Context, opts fwPortOptions, req BuiltinRequest) error {
|
||||
name := fmt.Sprintf("kopsc-allow-%d-%s", opts.port, opts.protocol)
|
||||
fmt.Fprintf(req.Stdout, "开放端口: %d/%s (Windows Firewall)\n", opts.port, opts.protocol)
|
||||
return runCommand(ctx, "netsh", []string{
|
||||
"advfirewall", "firewall", "add", "rule",
|
||||
"name=" + name, "dir=in", "action=allow",
|
||||
"protocol=" + opts.protocol, "localport=" + fmt.Sprintf("%d", opts.port),
|
||||
}, req)
|
||||
}
|
||||
|
||||
// runFwDeny implements `kopsc fw deny <port>/<protocol>`: close a port.
|
||||
func runFwDeny(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFwPortArgs(req.Args, "deny")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return fwDenyLinux(ctx, opts, req)
|
||||
case "darwin":
|
||||
return fmt.Errorf("macOS 不支持通过命令行关闭端口,请在系统偏好设置中配置")
|
||||
case "windows":
|
||||
return fwDenyWindows(ctx, opts, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func fwDenyLinux(ctx context.Context, opts fwPortOptions, req BuiltinRequest) error {
|
||||
portProto := fmt.Sprintf("%d/%s", opts.port, opts.protocol)
|
||||
if path, err := exec.LookPath("ufw"); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "关闭端口: %s (ufw)\n", portProto)
|
||||
return runCommand(ctx, path, []string{"deny", portProto}, req)
|
||||
}
|
||||
if path, err := exec.LookPath("firewall-cmd"); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "关闭端口: %s (firewalld)\n", portProto)
|
||||
if err := runCommand(ctx, path, []string{"--remove-port=" + portProto, "--permanent"}, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCommand(ctx, path, []string{"--reload"}, req)
|
||||
}
|
||||
return fmt.Errorf("未找到 ufw 或 firewall-cmd")
|
||||
}
|
||||
|
||||
func fwDenyWindows(ctx context.Context, opts fwPortOptions, req BuiltinRequest) error {
|
||||
name := fmt.Sprintf("kopsc-allow-%d-%s", opts.port, opts.protocol)
|
||||
fmt.Fprintf(req.Stdout, "关闭端口: %d/%s (Windows Firewall)\n", opts.port, opts.protocol)
|
||||
return runCommand(ctx, "netsh", []string{
|
||||
"advfirewall", "firewall", "delete", "rule",
|
||||
"name=" + name,
|
||||
}, req)
|
||||
}
|
||||
|
||||
type fwPortOptions struct {
|
||||
port int
|
||||
protocol string
|
||||
}
|
||||
|
||||
func parseFwPortArgs(args []string, action string) (fwPortOptions, error) {
|
||||
opts := fwPortOptions{protocol: "tcp"}
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-p" || arg == "--protocol":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少协议", arg)
|
||||
}
|
||||
i++
|
||||
proto := strings.ToLower(args[i])
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return opts, fmt.Errorf("协议必须是 tcp 或 udp,收到 %q", args[i])
|
||||
}
|
||||
opts.protocol = proto
|
||||
case strings.HasPrefix(arg, "--protocol="):
|
||||
proto := strings.ToLower(strings.TrimPrefix(arg, "--protocol="))
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return opts, fmt.Errorf("协议必须是 tcp 或 udp,收到 %q", strings.TrimPrefix(arg, "--protocol="))
|
||||
}
|
||||
opts.protocol = proto
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少端口号;用法: kopsc fw %s <端口>[/<协议>]", action)
|
||||
}
|
||||
|
||||
portStr := positional[0]
|
||||
if idx := strings.Index(portStr, "/"); idx >= 0 {
|
||||
proto := strings.ToLower(portStr[idx+1:])
|
||||
if proto != "tcp" && proto != "udp" {
|
||||
return opts, fmt.Errorf("协议必须是 tcp 或 udp,收到 %q", proto)
|
||||
}
|
||||
opts.protocol = proto
|
||||
portStr = portStr[:idx]
|
||||
}
|
||||
|
||||
n, err := parseInt(portStr)
|
||||
if err != nil || n < 1 || n > 65535 {
|
||||
return opts, fmt.Errorf("端口必须是 1-65535 的整数,收到 %q", positional[0])
|
||||
}
|
||||
opts.port = n
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func parseInt(s string) (int, error) {
|
||||
n := 0
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, fmt.Errorf("不是整数: %q", s)
|
||||
}
|
||||
n = n*10 + int(c-'0')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runLogSearch implements `kopsc log search <keyword>`: search system logs.
|
||||
func runLogSearch(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseLogSearchArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return logSearchLinux(ctx, opts, req)
|
||||
case "darwin":
|
||||
return logSearchDarwin(ctx, opts, req)
|
||||
case "windows":
|
||||
return logSearchWindows(ctx, opts, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
type logSearchOptions struct {
|
||||
keyword string
|
||||
since string
|
||||
unit string
|
||||
lines int
|
||||
}
|
||||
|
||||
func parseLogSearchArgs(args []string) (logSearchOptions, error) {
|
||||
opts := logSearchOptions{lines: 100}
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "--since":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("--since 缺少时长,例如 --since 1h")
|
||||
}
|
||||
i++
|
||||
opts.since = args[i]
|
||||
case strings.HasPrefix(arg, "--since="):
|
||||
opts.since = strings.TrimPrefix(arg, "--since=")
|
||||
case arg == "-u" || arg == "--unit":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少服务名", arg)
|
||||
}
|
||||
i++
|
||||
opts.unit = args[i]
|
||||
case strings.HasPrefix(arg, "--unit="):
|
||||
opts.unit = strings.TrimPrefix(arg, "--unit=")
|
||||
case arg == "-n" || arg == "--lines":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少行数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.lines = n
|
||||
case strings.HasPrefix(arg, "--lines="):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--lines="))
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数")
|
||||
}
|
||||
opts.lines = n
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少搜索关键字;用法: kopsc log search <关键字>")
|
||||
}
|
||||
opts.keyword = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func logSearchLinux(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
|
||||
args := []string{"--no-pager", "-n", strconv.Itoa(opts.lines), "--grep=" + opts.keyword}
|
||||
if opts.since != "" {
|
||||
since, err := normalizeSince(opts.since)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--since: %w", err)
|
||||
}
|
||||
args = append(args, "--since="+since)
|
||||
}
|
||||
if opts.unit != "" {
|
||||
args = append(args, "-u", opts.unit)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func logSearchDarwin(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
|
||||
predicate := fmt.Sprintf(`eventMessage CONTAINS[c] "%s"`, opts.keyword)
|
||||
if opts.unit != "" {
|
||||
predicate = fmt.Sprintf(`(subsystem == "%s" OR process == "%s") AND %s`, opts.unit, opts.unit, predicate)
|
||||
}
|
||||
since := "1h"
|
||||
if opts.since != "" {
|
||||
since = opts.since
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "log", "show", "--predicate", predicate, "--last", since, "--style", "compact")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func logSearchWindows(ctx context.Context, opts logSearchOptions, req BuiltinRequest) error {
|
||||
args := []string{"qe", "System", "/q:*[System[Provider]]", "/c:" + strconv.Itoa(opts.lines), "/rd:true", "/f:text"}
|
||||
cmd := exec.CommandContext(ctx, "wevtutil", args...)
|
||||
var stdout strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout.String()))
|
||||
count := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(strings.ToLower(line), strings.ToLower(opts.keyword)) {
|
||||
fmt.Fprintln(req.Stdout, line)
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
fmt.Fprintf(req.Stdout, "没有找到包含 %q 的日志\n", opts.keyword)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeSince converts shorthand like "1h", "30m" to journalctl --since format.
|
||||
func normalizeSince(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("时长不能为空")
|
||||
}
|
||||
if d, err := time.ParseDuration(s); err == nil {
|
||||
t := time.Now().Add(-d)
|
||||
return t.Format("2006-01-02 15:04:05"), nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// runLogLast implements `kopsc log last`: show recent system log entries.
|
||||
func runLogLast(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseLogLastArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return logLastLinux(ctx, opts, req)
|
||||
case "darwin":
|
||||
return logLastDarwin(ctx, opts, req)
|
||||
case "windows":
|
||||
return logLastWindows(ctx, opts, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
type logLastOptions struct {
|
||||
lines int
|
||||
unit string
|
||||
}
|
||||
|
||||
func parseLogLastArgs(args []string) (logLastOptions, error) {
|
||||
opts := logLastOptions{lines: 50}
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
remaining := args[i+1:]
|
||||
if len(remaining) > 0 {
|
||||
n, err := strconv.Atoi(remaining[0])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("行数需要正整数,收到 %q", remaining[0])
|
||||
}
|
||||
opts.lines = n
|
||||
}
|
||||
i = len(args)
|
||||
case arg == "-n" || arg == "--lines":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少行数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.lines = n
|
||||
case strings.HasPrefix(arg, "--lines="):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--lines="))
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数")
|
||||
}
|
||||
opts.lines = n
|
||||
case arg == "-u" || arg == "--unit":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少服务名", arg)
|
||||
}
|
||||
i++
|
||||
opts.unit = args[i]
|
||||
case strings.HasPrefix(arg, "--unit="):
|
||||
opts.unit = strings.TrimPrefix(arg, "--unit=")
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
n, err := strconv.Atoi(arg)
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("行数需要正整数,收到 %q", arg)
|
||||
}
|
||||
opts.lines = n
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func logLastLinux(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
|
||||
args := []string{"--no-pager", "-n", strconv.Itoa(opts.lines)}
|
||||
if opts.unit != "" {
|
||||
args = append(args, "-u", opts.unit)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func logLastDarwin(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
|
||||
predicate := `eventMessage != ""`
|
||||
if opts.unit != "" {
|
||||
predicate = fmt.Sprintf(`subsystem == "%s" OR process == "%s"`, opts.unit, opts.unit)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "log", "show", "--predicate", predicate, "--last", "5m", "--style", "compact")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func logLastWindows(ctx context.Context, opts logLastOptions, req BuiltinRequest) error {
|
||||
cmd := exec.CommandContext(ctx, "wevtutil", "qe", "System",
|
||||
"/c:"+strconv.Itoa(opts.lines), "/rd:true", "/f:text")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runNetPing implements `kopsc net ping <host>`: connectivity check.
|
||||
func runNetPing(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseNetPingArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := []string{}
|
||||
if opts.count > 0 {
|
||||
if runtime.GOOS == "windows" {
|
||||
args = append(args, "-n", strconv.Itoa(opts.count))
|
||||
} else {
|
||||
args = append(args, "-c", strconv.Itoa(opts.count))
|
||||
}
|
||||
}
|
||||
if opts.interval > 0 {
|
||||
if runtime.GOOS != "windows" {
|
||||
args = append(args, "-i", strconv.Itoa(opts.interval))
|
||||
}
|
||||
}
|
||||
args = append(args, opts.host)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "ping", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type netPingOptions struct {
|
||||
host string
|
||||
count int
|
||||
interval int
|
||||
}
|
||||
|
||||
func parseNetPingArgs(args []string) (netPingOptions, error) {
|
||||
opts := netPingOptions{count: 4}
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-c" || arg == "-n" || arg == "--count":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少次数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("次数需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.count = n
|
||||
case arg == "-i" || arg == "--interval":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少间隔秒数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("间隔需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.interval = n
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少目标主机;用法: kopsc net ping <主机>")
|
||||
}
|
||||
opts.host = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// runNetTrace implements `kopsc net trace <host>`: route tracing.
|
||||
func runNetTrace(ctx context.Context, req BuiltinRequest) error {
|
||||
if len(req.Args) == 0 {
|
||||
return fmt.Errorf("缺少目标主机;用法: kopsc net trace <主机>")
|
||||
}
|
||||
host := req.Args[0]
|
||||
|
||||
// Try mtr first (best output), then traceroute, then tracert on Windows.
|
||||
if path, err := exec.LookPath("mtr"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, path, "--report", "--report-wide", host)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd := exec.CommandContext(ctx, "tracert", host)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
if path, err := exec.LookPath("traceroute"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, path, host)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
return fmt.Errorf("未找到 traceroute 或 mtr,请安装其中之一")
|
||||
}
|
||||
|
||||
// runNetDNS implements `kopsc net dns <host>`: DNS lookup.
|
||||
func runNetDNS(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseNetDNSArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Try dig first, then nslookup, then host.
|
||||
if path, err := exec.LookPath("dig"); err == nil {
|
||||
args := []string{opts.host}
|
||||
if opts.recordType != "" {
|
||||
args = []string{opts.host, opts.recordType}
|
||||
}
|
||||
if opts.server != "" {
|
||||
args = append(args, "@"+opts.server)
|
||||
}
|
||||
args = append(args, "+noall", "+answer")
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
if path, err := exec.LookPath("nslookup"); err == nil {
|
||||
args := []string{opts.host}
|
||||
if opts.server != "" {
|
||||
args = []string{opts.host, opts.server}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
if path, err := exec.LookPath("host"); err == nil {
|
||||
args := []string{opts.host}
|
||||
if opts.server != "" {
|
||||
args = append(args, opts.server)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
return fmt.Errorf("未找到 dig、nslookup 或 host,请安装其中之一")
|
||||
}
|
||||
|
||||
type netDNSOptions struct {
|
||||
host string
|
||||
recordType string
|
||||
server string
|
||||
}
|
||||
|
||||
func parseNetDNSArgs(args []string) (netDNSOptions, error) {
|
||||
var opts netDNSOptions
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-t" || arg == "--type":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少记录类型", arg)
|
||||
}
|
||||
i++
|
||||
opts.recordType = strings.ToUpper(args[i])
|
||||
case strings.HasPrefix(arg, "--type="):
|
||||
opts.recordType = strings.ToUpper(strings.TrimPrefix(arg, "--type="))
|
||||
case arg == "-s" || arg == "--server":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少 DNS 服务器", arg)
|
||||
}
|
||||
i++
|
||||
opts.server = args[i]
|
||||
case strings.HasPrefix(arg, "--server="):
|
||||
opts.server = strings.TrimPrefix(arg, "--server=")
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少域名;用法: kopsc net dns <域名>")
|
||||
}
|
||||
opts.host = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// runNetCurl implements `kopsc net curl <url>`: HTTP diagnostics showing
|
||||
// status code, time breakdown, and redirect chain.
|
||||
func runNetCurl(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseNetCurlArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("curl"); err != nil {
|
||||
return fmt.Errorf("net curl 需要 curl,请先安装")
|
||||
}
|
||||
|
||||
args := []string{"-sS", "-o", "/dev/null", "-w",
|
||||
"HTTP 状态码: %{http_code}\n" +
|
||||
"重定向次数: %{num_redirects}\n" +
|
||||
"DNS 解析: %{time_namelookup}s\n" +
|
||||
"TCP 连接: %{time_connect}s\n" +
|
||||
"TLS 握手: %{time_appconnect}s\n" +
|
||||
"首字节: %{time_starttransfer}s\n" +
|
||||
"总耗时: %{time_total}s\n" +
|
||||
"下载大小: %{size_download} bytes\n" +
|
||||
"下载速度: %{speed_download} bytes/s\n",
|
||||
}
|
||||
if opts.follow {
|
||||
args = append(args, "-L")
|
||||
}
|
||||
if opts.method != "" {
|
||||
args = append(args, "-X", opts.method)
|
||||
}
|
||||
for _, h := range opts.headers {
|
||||
args = append(args, "-H", h)
|
||||
}
|
||||
args = append(args, opts.url)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "curl", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type netCurlOptions struct {
|
||||
url string
|
||||
follow bool
|
||||
method string
|
||||
headers []string
|
||||
}
|
||||
|
||||
func parseNetCurlArgs(args []string) (netCurlOptions, error) {
|
||||
var opts netCurlOptions
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-L" || arg == "--follow":
|
||||
opts.follow = true
|
||||
case arg == "-X" || arg == "--method":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少 HTTP 方法", arg)
|
||||
}
|
||||
i++
|
||||
opts.method = strings.ToUpper(args[i])
|
||||
case arg == "-H" || arg == "--header":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少 header 值", arg)
|
||||
}
|
||||
i++
|
||||
opts.headers = append(opts.headers, args[i])
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少 URL;用法: kopsc net curl <URL>")
|
||||
}
|
||||
opts.url = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runProcTopMem implements `kopsc proc topmem`: show top N processes by memory.
|
||||
func runProcTopMem(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseProcTopArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
procs, err := listProcessesWithMem(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(procs, func(i, j int) bool { return procs[i].mem > procs[j].mem })
|
||||
printProcTop(req, procs, opts.count, "内存")
|
||||
return nil
|
||||
}
|
||||
|
||||
// runProcTopCPU implements `kopsc proc topcpu`: show top N processes by CPU.
|
||||
func runProcTopCPU(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseProcTopArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
procs, err := listProcessesWithCPU(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(procs, func(i, j int) bool { return procs[i].cpu > procs[j].cpu })
|
||||
printProcTop(req, procs, opts.count, "CPU")
|
||||
return nil
|
||||
}
|
||||
|
||||
type procTopOptions struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func parseProcTopArgs(args []string) (procTopOptions, error) {
|
||||
opts := procTopOptions{count: 10}
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
remaining := args[i+1:]
|
||||
if len(remaining) > 0 {
|
||||
n, err := strconv.Atoi(remaining[0])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("数量需要正整数,收到 %q", remaining[0])
|
||||
}
|
||||
opts.count = n
|
||||
}
|
||||
i = len(args)
|
||||
case arg == "-n" || arg == "--count":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少数量", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--count 需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.count = n
|
||||
case strings.HasPrefix(arg, "--count="):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--count="))
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--count 需要正整数")
|
||||
}
|
||||
opts.count = n
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
n, err := strconv.Atoi(arg)
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("数量需要正整数,收到 %q", arg)
|
||||
}
|
||||
opts.count = n
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
type procEntry struct {
|
||||
PID int
|
||||
Name string
|
||||
Command string
|
||||
mem float64
|
||||
cpu float64
|
||||
}
|
||||
|
||||
func listProcessesWithMem(ctx context.Context) ([]procEntry, error) {
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
return listProcMemPS(ctx)
|
||||
}
|
||||
return listProcMemTasklist(ctx)
|
||||
}
|
||||
|
||||
func listProcessesWithCPU(ctx context.Context) ([]procEntry, error) {
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
return listProcCPUPS(ctx)
|
||||
}
|
||||
return listProcCPUTasklist(ctx)
|
||||
}
|
||||
|
||||
func listProcMemPS(ctx context.Context) ([]procEntry, error) {
|
||||
out, err := exec.CommandContext(ctx, "ps", "-eo", "pid=,rss=,comm=,args=").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ps 执行失败: %w", err)
|
||||
}
|
||||
var procs []procEntry
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
pid, _ := strconv.Atoi(fields[0])
|
||||
rss, _ := strconv.ParseFloat(fields[1], 64)
|
||||
name := fields[2]
|
||||
command := strings.Join(fields[3:], " ")
|
||||
if pid > 0 {
|
||||
procs = append(procs, procEntry{PID: pid, Name: name, Command: command, mem: rss})
|
||||
}
|
||||
}
|
||||
return procs, nil
|
||||
}
|
||||
|
||||
func listProcCPUPS(ctx context.Context) ([]procEntry, error) {
|
||||
out, err := exec.CommandContext(ctx, "ps", "-eo", "pid=,%cpu=,comm=,args=").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ps 执行失败: %w", err)
|
||||
}
|
||||
var procs []procEntry
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
pid, _ := strconv.Atoi(fields[0])
|
||||
cpu, _ := strconv.ParseFloat(fields[1], 64)
|
||||
name := fields[2]
|
||||
command := strings.Join(fields[3:], " ")
|
||||
if pid > 0 {
|
||||
procs = append(procs, procEntry{PID: pid, Name: name, Command: command, cpu: cpu})
|
||||
}
|
||||
}
|
||||
return procs, nil
|
||||
}
|
||||
|
||||
func listProcMemTasklist(ctx context.Context) ([]procEntry, error) {
|
||||
out, err := exec.CommandContext(ctx, "tasklist", "/fo", "csv", "/nh").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tasklist 执行失败: %w", err)
|
||||
}
|
||||
var procs []procEntry
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
fields := strings.Split(strings.TrimSpace(line), ",")
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
name := strings.Trim(fields[0], `"`)
|
||||
pid, _ := strconv.Atoi(strings.Trim(fields[1], `"`))
|
||||
memStr := strings.Trim(fields[4], ` "K`)
|
||||
memStr = strings.ReplaceAll(memStr, ",", "")
|
||||
mem, _ := strconv.ParseFloat(memStr, 64)
|
||||
if pid > 0 {
|
||||
procs = append(procs, procEntry{PID: pid, Name: name, mem: mem})
|
||||
}
|
||||
}
|
||||
return procs, nil
|
||||
}
|
||||
|
||||
func listProcCPUTasklist(ctx context.Context) ([]procEntry, error) {
|
||||
return nil, fmt.Errorf("Windows 下 tasklist 不报告 CPU 使用率,请使用任务管理器")
|
||||
}
|
||||
|
||||
func printProcTop(req BuiltinRequest, procs []procEntry, count int, label string) {
|
||||
if len(procs) == 0 {
|
||||
fmt.Fprintln(req.Stdout, "没有获取到进程信息")
|
||||
return
|
||||
}
|
||||
if count > len(procs) {
|
||||
count = len(procs)
|
||||
}
|
||||
w := req.Stdout
|
||||
fmt.Fprintf(w, "=== 占用 %s 前 %d 的进程 ===\n\n", label, count)
|
||||
fmt.Fprintf(w, "%-8s %-8s %-20s %s\n", "PID", label, "进程", "命令")
|
||||
for i := 0; i < count; i++ {
|
||||
p := procs[i]
|
||||
memStr := ""
|
||||
if label == "内存" {
|
||||
memStr = humanSize(int64(p.mem * 1024))
|
||||
} else {
|
||||
memStr = fmt.Sprintf("%.1f%%", p.cpu)
|
||||
}
|
||||
cmd := p.Command
|
||||
if len(cmd) > 50 {
|
||||
cmd = cmd[:50] + "..."
|
||||
}
|
||||
fmt.Fprintf(w, "%-8d %-8s %-20s %s\n", p.PID, memStr, p.Name, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// runProcKillName implements `kopsc proc killname <name>`: kill processes by name.
|
||||
func runProcKillName(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseProcKillNameArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
procs, err := listProcesses(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var targets []processInfo
|
||||
for _, p := range procs {
|
||||
if strings.EqualFold(p.Name, opts.name) || strings.Contains(strings.ToLower(p.Command), strings.ToLower(opts.name)) {
|
||||
targets = append(targets, p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
fmt.Fprintf(req.Stdout, "没有找到名为 %q 的进程\n", opts.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(req.Stdout, "找到 %d 个匹配进程:\n", len(targets))
|
||||
for _, p := range targets {
|
||||
fmt.Fprintf(req.Stdout, " %d %s %s\n", p.PID, p.Name, p.Command)
|
||||
}
|
||||
|
||||
if !opts.confirmed {
|
||||
fmt.Fprintf(req.Stdout, "\n这是破坏性操作,未执行。加 --yes 确认终止以上进程\n")
|
||||
return &ExitError{Code: 2}
|
||||
}
|
||||
|
||||
for _, p := range targets {
|
||||
if err := killPID(p.PID); err != nil {
|
||||
fmt.Fprintf(req.Stderr, "终止 PID %d 失败: %v\n", p.PID, err)
|
||||
} else {
|
||||
fmt.Fprintf(req.Stdout, "已终止 PID %d (%s)\n", p.PID, p.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type procKillNameOptions struct {
|
||||
name string
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
func parseProcKillNameArgs(args []string) (procKillNameOptions, error) {
|
||||
var opts procKillNameOptions
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-y" || arg == "--yes":
|
||||
opts.confirmed = true
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少进程名;用法: kopsc proc killname <进程名>")
|
||||
}
|
||||
opts.name = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func killPID(pid int) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F").Run()
|
||||
}
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return proc.Kill()
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runSvcStatus implements `kopsc svc status <service>`: show service status
|
||||
// and recent log lines in one step.
|
||||
func runSvcStatus(ctx context.Context, req BuiltinRequest) error {
|
||||
if len(req.Args) == 0 {
|
||||
return fmt.Errorf("缺少服务名;用法: kopsc svc status <服务名>")
|
||||
}
|
||||
name := req.Args[0]
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return svcStatusLinux(ctx, name, req)
|
||||
case "darwin":
|
||||
return svcStatusDarwin(ctx, name, req)
|
||||
case "windows":
|
||||
return svcStatusWindows(ctx, name, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func svcStatusLinux(ctx context.Context, name string, req BuiltinRequest) error {
|
||||
w := req.Stdout
|
||||
fmt.Fprintf(w, "=== 服务状态: %s ===\n\n", name)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "systemctl", "status", name, "--no-pager")
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = req.Stderr
|
||||
_ = cmd.Run()
|
||||
|
||||
fmt.Fprintf(w, "\n=== 最近日志 ===\n\n")
|
||||
logCmd := exec.CommandContext(ctx, "journalctl", "-u", name, "-n", "20", "--no-pager")
|
||||
logCmd.Stdout = w
|
||||
logCmd.Stderr = req.Stderr
|
||||
_ = logCmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func svcStatusDarwin(ctx context.Context, name string, req BuiltinRequest) error {
|
||||
w := req.Stdout
|
||||
fmt.Fprintf(w, "=== 服务状态: %s ===\n\n", name)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "launchctl", "list", name)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
_ = cmd.Run()
|
||||
|
||||
if stdout.Len() > 0 {
|
||||
fmt.Fprintln(w, stdout.String())
|
||||
} else {
|
||||
fmt.Fprintf(w, "服务 %q 未在 launchctl 中注册\n", name)
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
fmt.Fprintln(req.Stderr, stderr.String())
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n=== 日志(最近 20 行)===\n\n")
|
||||
logCmd := exec.CommandContext(ctx, "log", "show", "--predicate",
|
||||
fmt.Sprintf(`subsystem == "%s" OR process == "%s"`, name, name),
|
||||
"--last", "5m", "--style", "compact")
|
||||
logCmd.Stdout = w
|
||||
logCmd.Stderr = req.Stderr
|
||||
_ = logCmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func svcStatusWindows(ctx context.Context, name string, req BuiltinRequest) error {
|
||||
w := req.Stdout
|
||||
fmt.Fprintf(w, "=== 服务状态: %s ===\n\n", name)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sc", "query", name)
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = req.Stderr
|
||||
_ = cmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSvcEnable implements `kopsc svc enable <service>`: enable + start service.
|
||||
func runSvcEnable(ctx context.Context, req BuiltinRequest) error {
|
||||
if len(req.Args) == 0 {
|
||||
return fmt.Errorf("缺少服务名;用法: kopsc svc enable <服务名>")
|
||||
}
|
||||
name := req.Args[0]
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
fmt.Fprintf(req.Stdout, "启用并启动服务: %s\n", name)
|
||||
if err := runCommand(ctx, "systemctl", []string{"enable", name}, req); err != nil {
|
||||
return fmt.Errorf("启用服务失败: %w", err)
|
||||
}
|
||||
return runCommand(ctx, "systemctl", []string{"start", name}, req)
|
||||
case "darwin":
|
||||
fmt.Fprintf(req.Stdout, "加载并启动服务: %s\n", name)
|
||||
return runCommand(ctx, "launchctl", []string{"load", "-w", "/Library/LaunchDaemons/" + name + ".plist"}, req)
|
||||
case "windows":
|
||||
fmt.Fprintf(req.Stdout, "设置自启并启动服务: %s\n", name)
|
||||
if err := runCommand(ctx, "sc", []string{"config", name, "start=auto"}, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCommand(ctx, "sc", []string{"start", name}, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// runSvcDisable implements `kopsc svc disable <service>`: disable + stop service.
|
||||
func runSvcDisable(ctx context.Context, req BuiltinRequest) error {
|
||||
if len(req.Args) == 0 {
|
||||
return fmt.Errorf("缺少服务名;用法: kopsc svc disable <服务名>")
|
||||
}
|
||||
name := req.Args[0]
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
fmt.Fprintf(req.Stdout, "停止并禁用服务: %s\n", name)
|
||||
if err := runCommand(ctx, "systemctl", []string{"stop", name}, req); err != nil {
|
||||
return fmt.Errorf("停止服务失败: %w", err)
|
||||
}
|
||||
return runCommand(ctx, "systemctl", []string{"disable", name}, req)
|
||||
case "darwin":
|
||||
fmt.Fprintf(req.Stdout, "卸载服务: %s\n", name)
|
||||
return runCommand(ctx, "launchctl", []string{"unload", "-w", "/Library/LaunchDaemons/" + name + ".plist"}, req)
|
||||
case "windows":
|
||||
fmt.Fprintf(req.Stdout, "停止并禁用服务: %s\n", name)
|
||||
if err := runCommand(ctx, "sc", []string{"stop", name}, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return runCommand(ctx, "sc", []string{"config", name, "start=disabled"}, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// runSvcLogs implements `kopsc svc logs <service>`: follow service logs.
|
||||
func runSvcLogs(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseSvcLogsArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return svcLogsLinux(ctx, opts, req)
|
||||
case "darwin":
|
||||
return svcLogsDarwin(ctx, opts, req)
|
||||
case "windows":
|
||||
return svcLogsWindows(ctx, opts, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
type svcLogsOptions struct {
|
||||
service string
|
||||
follow bool
|
||||
lines int
|
||||
}
|
||||
|
||||
func parseSvcLogsArgs(args []string) (svcLogsOptions, error) {
|
||||
opts := svcLogsOptions{lines: 50}
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "-f" || arg == "--follow":
|
||||
opts.follow = true
|
||||
case arg == "--lines" || arg == "-n":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少行数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := strconv.Atoi(args[i])
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数,收到 %q", args[i])
|
||||
}
|
||||
opts.lines = n
|
||||
case strings.HasPrefix(arg, "--lines="):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(arg, "--lines="))
|
||||
if err != nil || n < 1 {
|
||||
return opts, fmt.Errorf("--lines 需要正整数")
|
||||
}
|
||||
opts.lines = n
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少服务名;用法: kopsc svc logs <服务名>")
|
||||
}
|
||||
opts.service = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func svcLogsLinux(ctx context.Context, opts svcLogsOptions, req BuiltinRequest) error {
|
||||
args := []string{"-u", opts.service, "--no-pager", "-n", strconv.Itoa(opts.lines)}
|
||||
if opts.follow {
|
||||
args = append(args, "-f")
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
cmd.Stdin = req.Stdin
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func svcLogsDarwin(ctx context.Context, opts svcLogsOptions, req BuiltinRequest) error {
|
||||
predicate := fmt.Sprintf(`subsystem == "%s" OR process == "%s"`, opts.service, opts.service)
|
||||
args := []string{"show", "--predicate", predicate, "--style", "compact", "--last", "5m"}
|
||||
cmd := exec.CommandContext(ctx, "log", args...)
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func svcLogsWindows(ctx context.Context, opts svcLogsOptions, req BuiltinRequest) error {
|
||||
cmd := exec.CommandContext(ctx, "wevtutil", "qe", "System",
|
||||
"/q:*[System[Provider[@Name='"+opts.service+"']]]",
|
||||
"/c:"+strconv.Itoa(opts.lines), "/rd:true", "/f:text")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/platform"
|
||||
)
|
||||
|
||||
// runSysInfo implements `kopsc sysinfo`: show OS, CPU, memory, disk overview.
|
||||
func runSysInfo(ctx context.Context, req BuiltinRequest) error {
|
||||
w := req.Stdout
|
||||
res := platform.DetectResources()
|
||||
|
||||
fmt.Fprintf(w, "=== 系统信息 ===\n\n")
|
||||
|
||||
fmt.Fprintf(w, "操作系统: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/etc/os-release"); err == nil {
|
||||
distro, _ := platform.ParseOSRelease(data)
|
||||
if distro != "" {
|
||||
fmt.Fprintf(w, "发行版: %s\n", distro)
|
||||
}
|
||||
}
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
fmt.Fprintf(w, "主机名: %s\n", hostname)
|
||||
}
|
||||
if data, err := os.ReadFile("/proc/version"); err == nil {
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) >= 3 {
|
||||
fmt.Fprintf(w, "内核: %s\n", fields[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n=== CPU ===\n\n")
|
||||
fmt.Fprintf(w, "逻辑核心: %d\n", res.CPUs)
|
||||
if runtime.GOOS == "linux" {
|
||||
if model := cpuModelLinux(); model != "" {
|
||||
fmt.Fprintf(w, "型号: %s\n", model)
|
||||
}
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
if model := cpuModelDarwin(ctx); model != "" {
|
||||
fmt.Fprintf(w, "型号: %s\n", model)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n=== 内存 ===\n\n")
|
||||
if res.Memory > 0 {
|
||||
fmt.Fprintf(w, "总计: %s\n", humanSize(res.Memory))
|
||||
fmt.Fprintf(w, "检测方式: %s\n", res.Detected)
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
if free := memFreeLinux(); free > 0 {
|
||||
fmt.Fprintf(w, "可用: %s\n", humanSize(free))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n=== 磁盘 ===\n\n")
|
||||
printDiskInfo(ctx, w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cpuModelLinux() string {
|
||||
data, err := os.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "model name") {
|
||||
_, value, ok := strings.Cut(line, ":")
|
||||
if ok {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cpuModelDarwin(ctx context.Context) string {
|
||||
out, err := exec.CommandContext(ctx, "sysctl", "-n", "machdep.cpu.brand_string").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func memFreeLinux() int64 {
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
if fields[0] == "MemAvailable:" || fields[0] == "MemFree:" {
|
||||
n, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n * 1024
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func printDiskInfo(ctx context.Context, w interface{ Write([]byte) (int, error) }) {
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd := exec.CommandContext(ctx, "wmic", "logicaldisk", "get", "size,freespace,caption", "/format:list")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "无法获取磁盘信息")
|
||||
return
|
||||
}
|
||||
parseWMICDisk(string(out), w)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "df", "-h")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "无法获取磁盘信息")
|
||||
return
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
for _, line := range lines {
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
}
|
||||
|
||||
func parseWMICDisk(out string, w interface{ Write([]byte) (int, error) }) {
|
||||
var caption, freeStr, sizeStr string
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Caption=") {
|
||||
caption = strings.TrimPrefix(line, "Caption=")
|
||||
} else if strings.HasPrefix(line, "FreeSpace=") {
|
||||
freeStr = strings.TrimPrefix(line, "FreeSpace=")
|
||||
} else if strings.HasPrefix(line, "Size=") {
|
||||
sizeStr = strings.TrimPrefix(line, "Size=")
|
||||
if caption != "" && sizeStr != "" {
|
||||
size, _ := strconv.ParseInt(sizeStr, 10, 64)
|
||||
free, _ := strconv.ParseInt(freeStr, 10, 64)
|
||||
fmt.Fprintf(w, "%-6s 总计 %-10s 可用 %-10s\n", caption, humanSize(size), humanSize(free))
|
||||
caption, freeStr, sizeStr = "", "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,9 +609,246 @@ func Builtin() *Registry {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ----- file -----
|
||||
{
|
||||
ID: "file.findbig",
|
||||
Group: "file",
|
||||
Summary: "查找大于指定阈值的文件(按大小排序)",
|
||||
Action: "递归扫描目录,找出所有大于阈值的常规文件,按大小从大到小排序输出。" +
|
||||
"默认阈值 100M,默认路径当前目录;--threshold 可指定大小(支持 K/M/G/T 后缀)",
|
||||
Usage: "[路径] [--threshold 大小]",
|
||||
Variants: threePlatformBuiltins("file.findbig"),
|
||||
},
|
||||
{
|
||||
ID: "file.findrecent",
|
||||
Group: "file",
|
||||
Summary: "查找最近 N 天内修改过的文件",
|
||||
Action: "递归扫描目录,找出最近 N 天内修改过的常规文件,按修改时间从新到旧排序输出。" +
|
||||
"默认 7 天,默认路径当前目录;--days 可指定天数",
|
||||
Usage: "[路径] [--days 天数]",
|
||||
Variants: threePlatformBuiltins("file.findrecent"),
|
||||
},
|
||||
{
|
||||
ID: "file.findname",
|
||||
Group: "file",
|
||||
Summary: "按文件名查找文件(忽略大小写)",
|
||||
Action: "递归扫描目录,按文件名模式匹配(支持通配符 * 和 ?,忽略大小写)。" +
|
||||
"默认路径当前目录",
|
||||
Usage: "<模式> [路径]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("file.findname"),
|
||||
},
|
||||
|
||||
// ----- svc -----
|
||||
{
|
||||
ID: "svc.status",
|
||||
Group: "svc",
|
||||
Summary: "查看服务状态与最近日志",
|
||||
Action: "一步查看服务运行状态和最近 20 行日志。" +
|
||||
"Linux: systemctl status + journalctl;macOS: launchctl list + log show;" +
|
||||
"Windows: sc query",
|
||||
Usage: "<服务名>",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("svc.status"),
|
||||
},
|
||||
{
|
||||
ID: "svc.enable",
|
||||
Group: "svc",
|
||||
Summary: "启用并启动服务(开机自启 + 立即启动)",
|
||||
Action: "将服务设为开机自启并立即启动。" +
|
||||
"Linux: systemctl enable + start;macOS: launchctl load -w;" +
|
||||
"Windows: sc config start=auto + sc start",
|
||||
Usage: "<服务名>",
|
||||
MinArgs: 1,
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("svc.enable"),
|
||||
},
|
||||
{
|
||||
ID: "svc.disable",
|
||||
Group: "svc",
|
||||
Summary: "禁用并停止服务",
|
||||
Action: "停止服务并取消开机自启。" +
|
||||
"Linux: systemctl stop + disable;macOS: launchctl unload -w;" +
|
||||
"Windows: sc stop + sc config start=disabled",
|
||||
Usage: "<服务名>",
|
||||
MinArgs: 1,
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("svc.disable"),
|
||||
},
|
||||
{
|
||||
ID: "svc.logs",
|
||||
Group: "svc",
|
||||
Summary: "查看服务日志(支持跟踪)",
|
||||
Action: "查看指定服务的日志。-f 可实时跟踪新日志,-n 指定行数(默认 50)。" +
|
||||
"Linux: journalctl -u;macOS: log show;Windows: wevtutil",
|
||||
Usage: "<服务名> [-f] [-n 行数]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("svc.logs"),
|
||||
},
|
||||
|
||||
// ----- archive -----
|
||||
{
|
||||
ID: "archive.extract",
|
||||
Aliases: []string{"archive.unzip", "archive.untar"},
|
||||
Group: "archive",
|
||||
Summary: "自动识别格式并解压压缩文件",
|
||||
Action: "根据文件扩展名自动选择解压方式:支持 .tar.gz / .tgz / .tar.bz2 / .tar.xz / .tar / .zip / .gz。" +
|
||||
"默认解压到当前目录,--dir 可指定目标目录",
|
||||
Usage: "<文件> [--dir 目录]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("archive.extract"),
|
||||
},
|
||||
|
||||
// ----- sysinfo -----
|
||||
{
|
||||
ID: "sysinfo",
|
||||
Group: "sys",
|
||||
Summary: "显示系统信息一览(OS、CPU、内存、磁盘)",
|
||||
Action: "一次性输出操作系统、CPU 型号与核心数、内存总量与可用量、磁盘使用情况。" +
|
||||
"Linux 读取 /proc 与 cgroup,macOS 使用 sysctl,Windows 使用 wmic",
|
||||
Variants: threePlatformBuiltins("sysinfo"),
|
||||
},
|
||||
|
||||
// ----- proc additions -----
|
||||
{
|
||||
ID: "proc.topmem",
|
||||
Group: "proc",
|
||||
Summary: "按内存占用排序显示进程(默认前 10)",
|
||||
Action: "列出内存占用最高的 N 个进程,按 RSS 降序排列。默认显示前 10,-n 可指定数量",
|
||||
Usage: "[数量]",
|
||||
Variants: threePlatformBuiltins("proc.topmem"),
|
||||
},
|
||||
{
|
||||
ID: "proc.topcpu",
|
||||
Group: "proc",
|
||||
Summary: "按 CPU 占用排序显示进程(默认前 10)",
|
||||
Action: "列出 CPU 占用最高的 N 个进程,按 %CPU 降序排列。默认显示前 10,-n 可指定数量。" +
|
||||
"Windows 不支持(tasklist 不报告 CPU 使用率)",
|
||||
Usage: "[数量]",
|
||||
Variants: threePlatformBuiltins("proc.topcpu"),
|
||||
},
|
||||
{
|
||||
ID: "proc.killname",
|
||||
Group: "proc",
|
||||
Summary: "按进程名终止进程",
|
||||
Action: "查找名称或命令行中包含指定关键字的所有进程并终止它们。" +
|
||||
"默认只列出匹配进程,需加 --yes 确认终止",
|
||||
Usage: "<进程名>",
|
||||
MinArgs: 1,
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("proc.killname"),
|
||||
},
|
||||
|
||||
// ----- net additions -----
|
||||
{
|
||||
ID: "net.ping",
|
||||
Group: "net",
|
||||
Summary: "测试与目标主机的连通性",
|
||||
Action: "向目标主机发送 ICMP 回显请求,检测网络连通性与延迟。" +
|
||||
"默认发送 4 个包,-c 可指定数量",
|
||||
Usage: "<主机> [-c 次数]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("net.ping"),
|
||||
},
|
||||
{
|
||||
ID: "net.trace",
|
||||
Group: "net",
|
||||
Summary: "路由追踪(显示数据包经过的每一跳)",
|
||||
Action: "追踪到达目标主机经过的每一跳路由。优先使用 mtr(最佳输出)," +
|
||||
"回退到 traceroute(Linux/macOS)或 tracert(Windows)",
|
||||
Usage: "<主机>",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("net.trace"),
|
||||
},
|
||||
{
|
||||
ID: "net.dns",
|
||||
Group: "net",
|
||||
Summary: "DNS 查询(解析域名)",
|
||||
Action: "查询域名的 DNS 记录。优先使用 dig,回退到 nslookup 或 host。" +
|
||||
"-t 指定记录类型(A/AAAA/MX/CNAME 等),-s 指定 DNS 服务器",
|
||||
Usage: "<域名> [-t 类型] [-s 服务器]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("net.dns"),
|
||||
},
|
||||
{
|
||||
ID: "net.curl",
|
||||
Group: "net",
|
||||
Summary: "HTTP 诊断(状态码、耗时、重定向链)",
|
||||
Action: "发送 HTTP 请求并显示诊断信息:状态码、DNS 解析耗时、TCP 连接耗时、TLS 握手耗时、" +
|
||||
"首字节耗时、总耗时、下载大小与速度。-L 跟随重定向,-X 指定方法,-H 添加 header",
|
||||
Usage: "<URL> [-L] [-X 方法] [-H header]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("net.curl"),
|
||||
},
|
||||
|
||||
// ----- log -----
|
||||
{
|
||||
ID: "log.search",
|
||||
Group: "log",
|
||||
Summary: "在系统日志中搜索关键字",
|
||||
Action: "在系统日志中搜索包含指定关键字的条目。" +
|
||||
"Linux: journalctl --grep;macOS: log show --predicate;Windows: wevtutil。" +
|
||||
"--since 限定时间范围(如 1h、30m),-u 限定服务,-n 限定行数",
|
||||
Usage: "<关键字> [--since 时长] [-u 服务] [-n 行数]",
|
||||
MinArgs: 1,
|
||||
Variants: threePlatformBuiltins("log.search"),
|
||||
},
|
||||
{
|
||||
ID: "log.last",
|
||||
Group: "log",
|
||||
Summary: "查看最近的系统日志",
|
||||
Action: "显示最近的系统日志条目。默认 50 行,-n 可指定行数,-u 可限定服务。" +
|
||||
"Linux: journalctl;macOS: log show;Windows: wevtutil",
|
||||
Usage: "[行数] [-u 服务]",
|
||||
Variants: threePlatformBuiltins("log.last"),
|
||||
},
|
||||
|
||||
// ----- fw -----
|
||||
{
|
||||
ID: "fw.status",
|
||||
Group: "fw",
|
||||
Summary: "查看防火墙状态与规则",
|
||||
Action: "显示当前防火墙状态与规则列表。" +
|
||||
"Linux: 自动检测 ufw / firewalld / iptables;macOS: ALF(应用防火墙);" +
|
||||
"Windows: netsh advfirewall",
|
||||
Variants: threePlatformBuiltins("fw.status"),
|
||||
},
|
||||
{
|
||||
ID: "fw.allow",
|
||||
Group: "fw",
|
||||
Summary: "开放防火墙端口",
|
||||
Action: "在防火墙中开放指定端口。支持 8080/tcp 或 8080/udp 形式,默认 tcp。" +
|
||||
"Linux: ufw allow / firewall-cmd --add-port;Windows: netsh advfirewall add rule",
|
||||
Usage: "<端口>[/<协议>]",
|
||||
MinArgs: 1,
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("fw.allow"),
|
||||
},
|
||||
{
|
||||
ID: "fw.deny",
|
||||
Group: "fw",
|
||||
Summary: "关闭防火墙端口",
|
||||
Action: "在防火墙中关闭指定端口。支持 8080/tcp 或 8080/udp 形式,默认 tcp。" +
|
||||
"Linux: ufw deny / firewall-cmd --remove-port;Windows: netsh advfirewall delete rule",
|
||||
Usage: "<端口>[/<协议>]",
|
||||
MinArgs: 1,
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("fw.deny"),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// threePlatformBuiltins builds the platform matrix for a command that kopsc
|
||||
// implements itself on all three platforms.
|
||||
func threePlatformBuiltins(name string) []Variant {
|
||||
return []Variant{
|
||||
{Match: Match{OS: "linux"}, Builtin: name, Note: "Linux 内置实现"},
|
||||
{Match: Match{OS: "darwin"}, Builtin: name, Note: "macOS 内置实现"},
|
||||
{Match: Match{OS: "windows"}, Builtin: name, Note: "Windows 内置实现"},
|
||||
}
|
||||
}
|
||||
|
||||
// runtimeBuiltinVariants builds the platform matrix for a container command
|
||||
// that kopsc implements itself. Builtin variants carry no argv; the builtin
|
||||
// learns the runtime's program name from Variant.Runtime instead.
|
||||
|
||||
@@ -27,6 +27,14 @@ func TestBuiltinIDsStable(t *testing.T) {
|
||||
"diagnose",
|
||||
"disk.usage", "net.pids", "net.ports", "proc.nohup", "proc.wait", "java.run", "git.retime", "pkg.refreshindex",
|
||||
"user.grant", "user.revoke",
|
||||
"file.findbig", "file.findrecent", "file.findname",
|
||||
"svc.status", "svc.enable", "svc.disable", "svc.logs",
|
||||
"archive.extract",
|
||||
"sysinfo",
|
||||
"proc.topmem", "proc.topcpu", "proc.killname",
|
||||
"net.ping", "net.trace", "net.dns", "net.curl",
|
||||
"log.search", "log.last",
|
||||
"fw.status", "fw.allow", "fw.deny",
|
||||
}
|
||||
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)
|
||||
|
||||
Reference in New Issue
Block a user