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
+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
}