zutils/cmd/usage.go

229 lines
5.5 KiB
Go
Raw Normal View History

package cmd
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/zemenawi/zutils/pkg/colors"
"github.com/zemenawi/zutils/pkg/formatter"
)
type ProcessInfo struct {
PID int
Name string
Command string
CPUPercent float64
MemBytes int64
MemPercent float64
Status string
User string
}
func UsageCommand(args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing process name\n\nUsage: z usage <process-name>\n\nExamples:\n z usage cron # Show cron process usage\n z usage mongod # Show MongoDB usage\n z usage chrome # Show all Chrome processes")
}
processName := args[0]
processes := findProcessesByName(processName)
if len(processes) == 0 {
fmt.Printf("\n %sNo processes found matching '%s'%s\n\n", colors.Yellow, processName, colors.Reset)
fmt.Printf(" Try a different process name, or use %sz sys%s to see all processes\n\n", colors.Gray, colors.Reset)
return nil
}
PrintBoxHeader(fmt.Sprintf("USAGE: %s (%d processes)", processName, len(processes)), colors.Cyan)
totalCPU := 0.0
totalMem := int64(0)
for _, p := range processes {
totalCPU += p.CPUPercent
totalMem += p.MemBytes
}
fmt.Printf("%s%s📊 Total CPU:%s %.1f%%\n", colors.Green, colors.Bold, colors.Reset, totalCPU)
fmt.Printf("%s%s💾 Total Memory:%s %s (%.1f MB)\n", colors.Purple, colors.Bold, colors.Reset, formatter.FormatSize(totalMem), float64(totalMem)/1024/1024)
fmt.Println()
headers := []string{"PID", "Process", "CPU%", "Memory", "Status"}
data := make([][]string, 0, len(processes))
sort.Slice(processes, func(i, j int) bool {
return processes[i].CPUPercent > processes[j].CPUPercent
})
for _, p := range processes {
cmd := p.Command
if len(cmd) > 30 {
parts := strings.Fields(cmd)
if len(parts) > 0 {
cmd = filepath.Base(parts[0]) + " " + strings.Join(parts[1:], " ")[:25]
}
}
status := p.Status
if status == "R" {
status = colors.Green + "running" + colors.Reset
} else if status == "S" || status == "I" {
status = colors.Gray + "sleeping" + colors.Reset
} else {
status = colors.Yellow + status + colors.Reset
}
row := []string{
fmt.Sprintf("%d", p.PID),
cmd,
fmt.Sprintf("%.1f", p.CPUPercent),
formatter.FormatSize(p.MemBytes),
status,
}
data = append(data, row)
}
formatter.PrintTable(headers, data, colors.Cyan)
fmt.Println()
return nil
}
func findProcessesByName(name string) []ProcessInfo {
var processes []ProcessInfo
dir, err := os.Open("/proc")
if err != nil {
return processes
}
defer dir.Close()
entries, err := dir.Readdirnames(500)
if err != nil {
return processes
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry)
if err != nil {
continue
}
proc, err := getProcessInfo(pid, name)
if err == nil && proc != nil {
processes = append(processes, *proc)
}
}
return processes
}
func getProcessInfo(pid int, targetName string) (*ProcessInfo, error) {
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
return nil, err
}
procName := strings.TrimSpace(string(comm))
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
if !strings.Contains(strings.ToLower(procName), strings.ToLower(targetName)) {
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
if cmdline != nil && !strings.Contains(strings.ToLower(string(cmdline)), strings.ToLower(targetName)) {
return nil, fmt.Errorf("name mismatch")
}
}
}
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
return nil, err
}
parts := strings.Split(string(stat), " ")
if len(parts) < 24 {
return nil, fmt.Errorf("invalid stat format")
}
status := "?"
if len(parts) > 2 {
status = strings.Trim(parts[2], "()")
}
utime := parseIntSafe(parts[13])
stime := parseIntSafe(parts[14])
memBytes := int64(0)
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "VmRSS:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
memKB := parseIntSafe(fields[1])
memBytes = memKB * 1024
}
break
}
}
}
totalMem := int64(0)
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "MemTotal:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
totalMem = parseIntSafe(fields[1]) * 1024
}
break
}
}
}
cpuPercent := 0.0
if totalMem > 0 {
cpuPercent = float64(utime+stime) / float64(totalMem) * 100
}
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
cmdStr := strings.Join(strings.Fields(string(cmdline)), " ")
return &ProcessInfo{
PID: pid,
Name: procName,
Command: cmdStr,
CPUPercent: cpuPercent,
MemBytes: memBytes,
MemPercent: float64(memBytes) / float64(totalMem) * 100,
Status: status,
User: getProcessUser(pid),
}, nil
}
func getProcessUser(pid int) string {
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "Uid:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
uid := parseIntSafe(fields[1])
return fmt.Sprintf("uid:%d", uid)
}
}
}
}
return "?"
}
func parseIntSafe(s string) int64 {
if i, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64); err == nil {
return i
}
return 0
}