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
+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 = "", "", ""
}
}
}
}