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:
2026-11-02 22:42:18 +08:00
parent 038926e2dd
commit 400812d114
21 changed files with 2870 additions and 14 deletions
+41
View File
@@ -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` | 目标目录(默认当前目录) |
+53
View File
@@ -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
| 选项 | 说明 |
| --- | --- |
| 第一个位置参数 | 文件名模式(必填) |
| 第二个位置参数 | 搜索路径(默认 `.`) |
+45
View File
@@ -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` |
+53
View File
@@ -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:` |
+32 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 结束。
+40
View File
@@ -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` |
+51
View File
@@ -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
View File
@@ -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`.
+272
View File
@@ -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 != ".."
}
+21
View File
@@ -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,
}
+388
View File
@@ -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
}
+239
View File
@@ -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
}
+269
View File
@@ -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()
}
+289
View File
@@ -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
}
+303
View File
@@ -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()
}
+240
View File
@@ -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()
}
+160
View File
@@ -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 = "", "", ""
}
}
}
}
+237
View File
@@ -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.
+8
View File
@@ -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)