feat: 新增 sys.uptime/net.iface/net.conn/net.speed/proc.tree/file.empty/user.info 命令
This commit is contained in:
@@ -47,6 +47,7 @@ var builtins = map[string]Builtin{
|
||||
"docker.save": runDockerSave,
|
||||
"docker.stats": runDockerStats,
|
||||
"docker.stop": runDockerStop,
|
||||
"file.empty": runFileEmpty,
|
||||
"file.findbig": runFileFindBig,
|
||||
"file.findname": runFileFindName,
|
||||
"file.findrecent": runFileFindRecent,
|
||||
@@ -59,23 +60,29 @@ var builtins = map[string]Builtin{
|
||||
"java.run.preview": runJavaPreview,
|
||||
"log.last": runLogLast,
|
||||
"log.search": runLogSearch,
|
||||
"net.conn": runNetConn,
|
||||
"net.curl": runNetCurl,
|
||||
"net.dns": runNetDNS,
|
||||
"net.iface": runNetIface,
|
||||
"net.ping": runNetPing,
|
||||
"net.pids": runNetPIDs,
|
||||
"net.ports": runNetPorts,
|
||||
"net.speed": runNetSpeed,
|
||||
"net.trace": runNetTrace,
|
||||
"proc.killname": runProcKillName,
|
||||
"proc.nohup": runNohup,
|
||||
"proc.topcpu": runProcTopCPU,
|
||||
"proc.topmem": runProcTopMem,
|
||||
"proc.tree": runProcTree,
|
||||
"proc.wait": runProcWait,
|
||||
"svc.disable": runSvcDisable,
|
||||
"svc.enable": runSvcEnable,
|
||||
"svc.logs": runSvcLogs,
|
||||
"svc.status": runSvcStatus,
|
||||
"sys.uptime": runSysUptime,
|
||||
"sysinfo": runSysInfo,
|
||||
"user.grant": runUserGrant,
|
||||
"user.info": runUserInfo,
|
||||
"user.revoke": runUserRevoke,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runFileEmpty implements `kopsc file empty`: find empty files and directories.
|
||||
func runFileEmpty(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseFileEmptyArgs(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 emptyFiles []string
|
||||
var emptyDirs []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() {
|
||||
// Check if directory is empty
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(entries) == 0 && path != root {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
if rel == "" {
|
||||
rel = path
|
||||
}
|
||||
emptyDirs = append(emptyDirs, rel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if file is empty
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
if rel == "" {
|
||||
rel = path
|
||||
}
|
||||
emptyFiles = append(emptyFiles, rel)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(emptyFiles)
|
||||
sort.Strings(emptyDirs)
|
||||
|
||||
if !opts.dirsOnly {
|
||||
if len(emptyFiles) == 0 {
|
||||
fmt.Fprintln(req.Stdout, "没有空文件")
|
||||
} else {
|
||||
fmt.Fprintf(req.Stdout, "=== 空文件 (%d 个) ===\n", len(emptyFiles))
|
||||
for _, f := range emptyFiles {
|
||||
fmt.Fprintln(req.Stdout, f)
|
||||
}
|
||||
fmt.Fprintln(req.Stdout)
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.filesOnly {
|
||||
if len(emptyDirs) == 0 {
|
||||
fmt.Fprintln(req.Stdout, "没有空目录")
|
||||
} else {
|
||||
fmt.Fprintf(req.Stdout, "=== 空目录 (%d 个) ===\n", len(emptyDirs))
|
||||
for _, d := range emptyDirs {
|
||||
fmt.Fprintln(req.Stdout, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileEmptyOptions struct {
|
||||
path string
|
||||
filesOnly bool
|
||||
dirsOnly bool
|
||||
}
|
||||
|
||||
func parseFileEmptyArgs(args []string) (fileEmptyOptions, error) {
|
||||
opts := fileEmptyOptions{path: "."}
|
||||
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 == "--files-only":
|
||||
opts.filesOnly = true
|
||||
case arg == "--dirs-only":
|
||||
opts.dirsOnly = true
|
||||
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]
|
||||
}
|
||||
if opts.filesOnly && opts.dirsOnly {
|
||||
return opts, fmt.Errorf("--files-only 和 --dirs-only 不能同时使用")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runNetConn implements `kopsc net conn`: show network connection statistics.
|
||||
func runNetConn(ctx context.Context, req BuiltinRequest) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return netConnLinux(ctx, req)
|
||||
case "darwin":
|
||||
return netConnDarwin(ctx, req)
|
||||
case "windows":
|
||||
return netConnWindows(ctx, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func netConnLinux(ctx context.Context, req BuiltinRequest) error {
|
||||
// Try ss first (faster)
|
||||
if path, err := exec.LookPath("ss"); err == nil {
|
||||
return netConnSS(ctx, path, req)
|
||||
}
|
||||
// Fallback to netstat
|
||||
return netConnNetstat(ctx, req)
|
||||
}
|
||||
|
||||
func netConnSS(ctx context.Context, path string, req BuiltinRequest) error {
|
||||
out, err := exec.CommandContext(ctx, path, "-tan").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ss 执行失败: %w", err)
|
||||
}
|
||||
return parseNetstatOutput(string(out), req)
|
||||
}
|
||||
|
||||
func netConnNetstat(ctx context.Context, req BuiltinRequest) error {
|
||||
out, err := exec.CommandContext(ctx, "netstat", "-tan").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstat 执行失败: %w", err)
|
||||
}
|
||||
return parseNetstatOutput(string(out), req)
|
||||
}
|
||||
|
||||
func netConnDarwin(ctx context.Context, req BuiltinRequest) error {
|
||||
out, err := exec.CommandContext(ctx, "netstat", "-tan").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstat 执行失败: %w", err)
|
||||
}
|
||||
return parseNetstatOutput(string(out), req)
|
||||
}
|
||||
|
||||
func netConnWindows(ctx context.Context, req BuiltinRequest) error {
|
||||
out, err := exec.CommandContext(ctx, "netstat", "-an").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstat 执行失败: %w", err)
|
||||
}
|
||||
return parseNetstatOutput(string(out), req)
|
||||
}
|
||||
|
||||
func parseNetstatOutput(output string, req BuiltinRequest) error {
|
||||
stats := make(map[string]int)
|
||||
total := 0
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect state column (usually last for TCP)
|
||||
state := fields[len(fields)-1]
|
||||
if isConnectionState(state) {
|
||||
stats[state]++
|
||||
total++
|
||||
}
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
fmt.Fprintln(req.Stdout, "没有活动的 TCP 连接")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(req.Stdout, "=== TCP 连接统计 (共 %d 个) ===\n\n", total)
|
||||
|
||||
// Sort by count descending
|
||||
type stateCount struct {
|
||||
state string
|
||||
count int
|
||||
}
|
||||
var sorted []stateCount
|
||||
for state, count := range stats {
|
||||
sorted = append(sorted, stateCount{state, count})
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].count > sorted[j].count
|
||||
})
|
||||
|
||||
for _, sc := range sorted {
|
||||
fmt.Fprintf(req.Stdout, "%-15s %d\n", sc.state, sc.count)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isConnectionState(s string) bool {
|
||||
switch s {
|
||||
case "ESTABLISHED", "SYN_SENT", "SYN_RECV", "FIN_WAIT1", "FIN_WAIT2",
|
||||
"TIME_WAIT", "CLOSE", "CLOSE_WAIT", "LAST_ACK", "LISTEN", "CLOSING":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// runNetIface implements `kopsc net iface`: show network interfaces and IPs.
|
||||
func runNetIface(ctx context.Context, req BuiltinRequest) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取网卡列表失败: %w", err)
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
// Skip loopback if not requested
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Status
|
||||
status := "DOWN"
|
||||
if iface.Flags&net.FlagUp != 0 {
|
||||
status = "UP"
|
||||
}
|
||||
|
||||
fmt.Fprintf(req.Stdout, "%s: <%s> mtu %d\n", iface.Name, status, iface.MTU)
|
||||
|
||||
// MAC address
|
||||
if len(iface.HardwareAddr) > 0 {
|
||||
fmt.Fprintf(req.Stdout, " MAC: %s\n", iface.HardwareAddr.String())
|
||||
}
|
||||
|
||||
// IP addresses
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
fmt.Fprintf(req.Stdout, " IP: %s\n", addr.String())
|
||||
}
|
||||
|
||||
// Flags
|
||||
fmt.Fprintf(req.Stdout, " 标志: %s\n", iface.Flags.String())
|
||||
fmt.Fprintln(req.Stdout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runNetSpeed implements `kopsc net speed`: simple network speed test.
|
||||
func runNetSpeed(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseNetSpeedArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(req.Stdout, "=== 网络速度测试 ===\n")
|
||||
|
||||
// Download test
|
||||
if !opts.uploadOnly {
|
||||
fmt.Fprintln(req.Stdout, "测试下载速度...")
|
||||
downloadSpeed, err := testDownload(ctx, opts.size)
|
||||
if err != nil {
|
||||
fmt.Fprintf(req.Stderr, "下载测试失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(req.Stdout, "下载速度: %s/s\n", humanSize(downloadSpeed))
|
||||
}
|
||||
fmt.Fprintln(req.Stdout)
|
||||
}
|
||||
|
||||
// Upload test
|
||||
if !opts.downloadOnly {
|
||||
fmt.Fprintln(req.Stdout, "测试上传速度...")
|
||||
uploadSpeed, err := testUpload(ctx, opts.size)
|
||||
if err != nil {
|
||||
fmt.Fprintf(req.Stderr, "上传测试失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(req.Stdout, "上传速度: %s/s\n", humanSize(uploadSpeed))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type netSpeedOptions struct {
|
||||
size int64
|
||||
downloadOnly bool
|
||||
uploadOnly bool
|
||||
}
|
||||
|
||||
func parseNetSpeedArgs(args []string) (netSpeedOptions, error) {
|
||||
opts := netSpeedOptions{size: 10 * 1024 * 1024} // 10MB default
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
remaining := args[i+1:]
|
||||
if len(remaining) > 0 {
|
||||
n, err := parseSizeArg(remaining[0])
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("大小参数无效: %w", err)
|
||||
}
|
||||
opts.size = n
|
||||
}
|
||||
i = len(args)
|
||||
case arg == "--size" || arg == "-s":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少大小参数", arg)
|
||||
}
|
||||
i++
|
||||
n, err := parseSizeArg(args[i])
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("--size: %w", err)
|
||||
}
|
||||
opts.size = n
|
||||
case strings.HasPrefix(arg, "--size="):
|
||||
n, err := parseSizeArg(strings.TrimPrefix(arg, "--size="))
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("--size: %w", err)
|
||||
}
|
||||
opts.size = n
|
||||
case arg == "--download" || arg == "-d":
|
||||
opts.downloadOnly = true
|
||||
case arg == "--upload" || arg == "-u":
|
||||
opts.uploadOnly = true
|
||||
default:
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.downloadOnly && opts.uploadOnly {
|
||||
return opts, fmt.Errorf("--download 和 --upload 不能同时使用")
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func testDownload(ctx context.Context, size int64) (int64, error) {
|
||||
// Use Cloudflare's speed test endpoint
|
||||
url := fmt.Sprintf("https://speed.cloudflare.com/__down?bytes=%d", size)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
n, err := io.Copy(io.Discard, resp.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
elapsed := time.Since(start).Seconds()
|
||||
if elapsed == 0 {
|
||||
elapsed = 0.001
|
||||
}
|
||||
return int64(float64(n) / elapsed), nil
|
||||
}
|
||||
|
||||
func testUpload(ctx context.Context, size int64) (int64, error) {
|
||||
// Use httpbin.org for upload test
|
||||
url := "https://httpbin.org/post"
|
||||
|
||||
// Create a reader that generates data on the fly
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = 'A'
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
elapsed := time.Since(start).Seconds()
|
||||
if elapsed == 0 {
|
||||
elapsed = 0.001
|
||||
}
|
||||
return int64(float64(size) / elapsed), nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
// runProcTree implements `kopsc proc tree`: show process tree.
|
||||
func runProcTree(ctx context.Context, req BuiltinRequest) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
return procTreeUnix(ctx, req)
|
||||
case "windows":
|
||||
return procTreeWindows(ctx, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func procTreeUnix(ctx context.Context, req BuiltinRequest) error {
|
||||
// Try pstree first (best output)
|
||||
if path, err := exec.LookPath("pstree"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, path, "-p")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Fallback: use ps with PPID and format a simple tree
|
||||
// macOS doesn't support -H, so we use a different approach
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd := exec.CommandContext(ctx, "ps", "-eo", "pid,ppid,comm")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Linux fallback: ps with hierarchical output
|
||||
cmd := exec.CommandContext(ctx, "ps", "-ejH")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func procTreeWindows(ctx context.Context, req BuiltinRequest) error {
|
||||
cmd := exec.CommandContext(ctx, "tasklist", "/tree")
|
||||
cmd.Stdout = req.Stdout
|
||||
cmd.Stderr = req.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// runSysUptime implements `kopsc sys uptime`: show system uptime.
|
||||
func runSysUptime(ctx context.Context, req BuiltinRequest) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return sysUptimeLinux(req)
|
||||
case "darwin":
|
||||
return sysUptimeDarwin(ctx, req)
|
||||
case "windows":
|
||||
return sysUptimeWindows(ctx, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func sysUptimeLinux(req BuiltinRequest) error {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 /proc/uptime 失败: %w", err)
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 1 {
|
||||
return fmt.Errorf("/proc/uptime 格式异常")
|
||||
}
|
||||
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析 uptime 失败: %w", err)
|
||||
}
|
||||
|
||||
bootTime := time.Now().Add(-time.Duration(seconds) * time.Second)
|
||||
fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(time.Duration(seconds)*time.Second))
|
||||
fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
// Load average
|
||||
if len(fields) >= 2 {
|
||||
idle, _ := strconv.ParseFloat(fields[1], 64)
|
||||
fmt.Fprintf(req.Stdout, "空闲时间: %s\n", formatDuration(time.Duration(idle)*time.Second))
|
||||
}
|
||||
|
||||
// Load average from /proc/loadavg
|
||||
if loadavg, err := os.ReadFile("/proc/loadavg"); err == nil {
|
||||
parts := strings.Fields(string(loadavg))
|
||||
if len(parts) >= 3 {
|
||||
fmt.Fprintf(req.Stdout, "负载均值: %s %s %s (1/5/15分钟)\n", parts[0], parts[1], parts[2])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sysUptimeDarwin(ctx context.Context, req BuiltinRequest) error {
|
||||
out, err := exec.CommandContext(ctx, "sysctl", "-n", "kern.boottime").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取启动时间失败: %w", err)
|
||||
}
|
||||
// Output: { sec = 1234567890, usec = 123456 } Mon Jan 01 00:00:00 2024
|
||||
line := string(out)
|
||||
secStart := strings.Index(line, "sec = ")
|
||||
if secStart < 0 {
|
||||
return fmt.Errorf("无法解析 kern.boottime: %s", line)
|
||||
}
|
||||
secStr := strings.TrimSpace(line[secStart+6:])
|
||||
if idx := strings.Index(secStr, ","); idx >= 0 {
|
||||
secStr = secStr[:idx]
|
||||
}
|
||||
sec, err := strconv.ParseInt(secStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析启动时间失败: %w", err)
|
||||
}
|
||||
|
||||
bootTime := time.Unix(sec, 0)
|
||||
uptime := time.Since(bootTime)
|
||||
fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(uptime))
|
||||
fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
// Load average
|
||||
if out, err := exec.CommandContext(ctx, "sysctl", "-n", "vm.loadavg").Output(); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "负载均值: %s\n", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sysUptimeWindows(ctx context.Context, req BuiltinRequest) error {
|
||||
// Use wmic to get boot time
|
||||
out, err := exec.CommandContext(ctx, "wmic", "os", "get", "LastBootUpTime", "/value").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取启动时间失败: %w", err)
|
||||
}
|
||||
line := strings.TrimSpace(string(out))
|
||||
// LastBootUpTime=20240101000000.000000+480
|
||||
if !strings.HasPrefix(line, "LastBootUpTime=") {
|
||||
return fmt.Errorf("无法解析 wmic 输出: %s", line)
|
||||
}
|
||||
timeStr := strings.TrimPrefix(line, "LastBootUpTime=")
|
||||
// Parse: 20240101000000.000000+480
|
||||
if len(timeStr) < 14 {
|
||||
return fmt.Errorf("时间格式异常: %s", timeStr)
|
||||
}
|
||||
bootTime, err := time.Parse("20060102150405", timeStr[:14])
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析时间失败: %w", err)
|
||||
}
|
||||
uptime := time.Since(bootTime)
|
||||
fmt.Fprintf(req.Stdout, "运行时间: %s\n", formatDuration(uptime))
|
||||
fmt.Fprintf(req.Stdout, "启动时间: %s\n", bootTime.Format("2006-01-02 15:04:05"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatDuration formats a duration into a human-readable string.
|
||||
func formatDuration(d time.Duration) string {
|
||||
days := int(d.Hours()) / 24
|
||||
hours := int(d.Hours()) % 24
|
||||
minutes := int(d.Minutes()) % 60
|
||||
|
||||
if days > 0 {
|
||||
return fmt.Sprintf("%d 天 %d 小时 %d 分钟", days, hours, minutes)
|
||||
}
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%d 小时 %d 分钟", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%d 分钟", minutes)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runUserInfo implements `kopsc user info`: show user details.
|
||||
func runUserInfo(ctx context.Context, req BuiltinRequest) error {
|
||||
var username string
|
||||
if len(req.Args) == 0 {
|
||||
// Current user
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取当前用户失败: %w", err)
|
||||
}
|
||||
username = u.Username
|
||||
} else {
|
||||
username = req.Args[0]
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
return userInfoUnix(ctx, username, req)
|
||||
case "windows":
|
||||
return userInfoWindows(ctx, username, req)
|
||||
default:
|
||||
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func userInfoUnix(ctx context.Context, username string, req BuiltinRequest) error {
|
||||
// Use id command for comprehensive info
|
||||
cmd := exec.CommandContext(ctx, "id", username)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户 %q 不存在或 id 命令失败: %w", username, err)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "用户: %s\n", strings.TrimSpace(string(out)))
|
||||
|
||||
// Get home directory and shell from /etc/passwd
|
||||
if data, err := os.ReadFile("/etc/passwd"); err == nil {
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) >= 7 && fields[0] == username {
|
||||
fmt.Fprintf(req.Stdout, "UID: %s\n", fields[2])
|
||||
fmt.Fprintf(req.Stdout, "GID: %s\n", fields[3])
|
||||
fmt.Fprintf(req.Stdout, "描述: %s\n", fields[4])
|
||||
fmt.Fprintf(req.Stdout, "家目录: %s\n", fields[5])
|
||||
fmt.Fprintf(req.Stdout, "Shell: %s\n", fields[6])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Groups
|
||||
groupsCmd := exec.CommandContext(ctx, "groups", username)
|
||||
if groupsOut, err := groupsCmd.Output(); err == nil {
|
||||
fmt.Fprintf(req.Stdout, "组: %s\n", strings.TrimSpace(string(groupsOut)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func userInfoWindows(ctx context.Context, username string, req BuiltinRequest) error {
|
||||
cmd := exec.CommandContext(ctx, "net", "user", username)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户 %q 不存在或 net user 失败: %w", username, err)
|
||||
}
|
||||
fmt.Fprintln(req.Stdout, string(out))
|
||||
return nil
|
||||
}
|
||||
@@ -836,6 +836,77 @@ func Builtin() *Registry {
|
||||
RequiresYes: true,
|
||||
Variants: threePlatformBuiltins("fw.deny"),
|
||||
},
|
||||
|
||||
// ----- sys additions -----
|
||||
{
|
||||
ID: "sys.uptime",
|
||||
Group: "sys",
|
||||
Summary: "查看系统运行时间和启动时间",
|
||||
Action: "显示系统已运行多长时间、启动时间、负载均值。" +
|
||||
"Linux: /proc/uptime + /proc/loadavg;macOS: sysctl kern.boottime;" +
|
||||
"Windows: wmic os get LastBootUpTime",
|
||||
Variants: threePlatformBuiltins("sys.uptime"),
|
||||
},
|
||||
|
||||
// ----- net additions -----
|
||||
{
|
||||
ID: "net.iface",
|
||||
Group: "net",
|
||||
Summary: "查看网卡信息和 IP 地址",
|
||||
Action: "列出所有非回环网卡的信息:名称、状态、MAC 地址、IPv4/IPv6 地址、MTU。" +
|
||||
"纯 Go 实现,跨平台一致",
|
||||
Variants: threePlatformBuiltins("net.iface"),
|
||||
},
|
||||
{
|
||||
ID: "net.conn",
|
||||
Group: "net",
|
||||
Summary: "查看 TCP 连接状态统计",
|
||||
Action: "统计各状态的 TCP 连接数量(ESTABLISHED、TIME_WAIT、CLOSE_WAIT 等)。" +
|
||||
"Linux: ss -s 或 netstat;macOS: netstat;Windows: netstat",
|
||||
Variants: threePlatformBuiltins("net.conn"),
|
||||
},
|
||||
{
|
||||
ID: "net.speed",
|
||||
Group: "net",
|
||||
Summary: "简单网速测试(下载/上传)",
|
||||
Action: "从 Cloudflare 下载测试文件测量下载速度,向 httpbin.org 上传数据测量上传速度。" +
|
||||
"默认测试 10MB,--size 可指定大小,--download 或 --upload 可单独测试",
|
||||
Usage: "[--size 大小] [--download] [--upload]",
|
||||
Variants: threePlatformBuiltins("net.speed"),
|
||||
},
|
||||
|
||||
// ----- proc additions -----
|
||||
{
|
||||
ID: "proc.tree",
|
||||
Group: "proc",
|
||||
Summary: "查看进程树(父子关系)",
|
||||
Action: "以树形结构显示进程的父子关系。" +
|
||||
"Linux/macOS: pstree -p 或 ps -ejH;Windows: tasklist /tree",
|
||||
Variants: threePlatformBuiltins("proc.tree"),
|
||||
},
|
||||
|
||||
// ----- file additions -----
|
||||
{
|
||||
ID: "file.empty",
|
||||
Group: "file",
|
||||
Summary: "查找空文件和空目录",
|
||||
Action: "递归扫描目录,找出大小为 0 的文件和没有任何内容的目录。" +
|
||||
"--files-only 只查找空文件,--dirs-only 只查找空目录",
|
||||
Usage: "[路径] [--files-only] [--dirs-only]",
|
||||
Variants: threePlatformBuiltins("file.empty"),
|
||||
},
|
||||
|
||||
// ----- user additions -----
|
||||
{
|
||||
ID: "user.info",
|
||||
Group: "user",
|
||||
Summary: "查看用户详细信息",
|
||||
Action: "显示用户的 UID、GID、组、家目录、Shell 等信息。" +
|
||||
"不指定用户名时显示当前用户。" +
|
||||
"Linux/macOS: id + /etc/passwd + groups;Windows: net user",
|
||||
Usage: "[用户名]",
|
||||
Variants: threePlatformBuiltins("user.info"),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ func TestBuiltinIDsStable(t *testing.T) {
|
||||
"net.ping", "net.trace", "net.dns", "net.curl",
|
||||
"log.search", "log.last",
|
||||
"fw.status", "fw.allow", "fw.deny",
|
||||
"sys.uptime",
|
||||
"net.iface", "net.conn", "net.speed",
|
||||
"proc.tree",
|
||||
"file.empty",
|
||||
"user.info",
|
||||
}
|
||||
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)
|
||||
|
||||
Reference in New Issue
Block a user