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:
+14
-8
@@ -216,14 +216,20 @@ func translateHelp(text string) string {
|
||||
// groupDescriptions explains what each command group is for. Without them
|
||||
// cobra would show "disk 相关命令", which tells a user nothing.
|
||||
var groupDescriptions = map[string]string{
|
||||
"docker": "Docker 容器与镜像管理(查看、启停、日志、清理、导出/导入、Compose)",
|
||||
"disk": "磁盘与文件占用:查看哪个目录、哪个文件最占空间",
|
||||
"net": "网络与端口:端口被谁占用、本机监听了哪些端口",
|
||||
"pkg": "系统软件包:刷新包管理器(dnf/apt/apk/brew/winget)的软件源索引",
|
||||
"proc": "本机进程:在后台运行命令并写日志,关掉终端也不退出",
|
||||
"java": "Java 应用:按 JDK 版本自动套用调优参数并前台运行 jar",
|
||||
"git": "Git 历史:把指定日期范围的提交时间改到 21:00-23:00 并强推",
|
||||
"user": "用户管理:授予或移除用户的 root/sudo 权限",
|
||||
"docker": "Docker 容器与镜像管理(查看、启停、日志、清理、导出/导入、Compose)",
|
||||
"disk": "磁盘与文件占用:查看哪个目录、哪个文件最占空间",
|
||||
"net": "网络与端口:端口被谁占用、本机监听了哪些端口、连通性、DNS、路由追踪、HTTP 诊断",
|
||||
"pkg": "系统软件包:刷新包管理器(dnf/apt/apk/brew/winget)的软件源索引",
|
||||
"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