feat: 新增 file/svc/archive/sys/log/fw 分组并扩充 net、proc
- file:findbig 按大小、findrecent 按修改时间、findname 按名称查找文件 - svc:systemctl / launchctl / sc 统一接口,status、enable、disable、logs - archive:按扩展名自动识别格式并解压,支持 tar.gz/tgz/tar.bz2/tar.xz/tar/zip/gz - sysinfo:OS、CPU、内存、磁盘一览 - log:journalctl / log show / wevtutil 搜索关键字与查看最近日志 - fw:ufw / firewalld / iptables / ALF / netsh 统一接口,status、allow、deny - net 新增 ping、trace(mtr 优先)、dns(dig 优先)、curl HTTP 诊断 - proc 新增 topmem、topcpu、killname - 危险操作(svc enable/disable、proc killname、fw allow/deny)需 --yes 确认 - 注册表、executor 派发表、cli 分组说明与 README 同步更新
This commit is contained in:
@@ -0,0 +1,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 = "", "", ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user