371 lines
9 KiB
Go
371 lines
9 KiB
Go
|
|
package cmd
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"os/exec"
|
|||
|
|
"runtime"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"syscall"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/zemenawi/zutils/pkg/colors"
|
|||
|
|
"github.com/zemenawi/zutils/pkg/formatter"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type SystemStats struct {
|
|||
|
|
Hostname string
|
|||
|
|
OS string
|
|||
|
|
Platform string
|
|||
|
|
CPUModel string
|
|||
|
|
CPUCount int
|
|||
|
|
Uptime string
|
|||
|
|
LoadAvg string
|
|||
|
|
MemTotal int64
|
|||
|
|
MemUsed int64
|
|||
|
|
MemFree int64
|
|||
|
|
DiskTotal int64
|
|||
|
|
DiskUsed int64
|
|||
|
|
DiskFree int64
|
|||
|
|
TopProcs []ProcInfo
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ProcInfo struct {
|
|||
|
|
PID int
|
|||
|
|
Name string
|
|||
|
|
CPU float64
|
|||
|
|
Mem float64
|
|||
|
|
Command string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func SystemCommand(args []string) error {
|
|||
|
|
stats, err := getSystemStats()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("error getting system stats: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
PrintBoxHeader("SYSTEM OVERVIEW", colors.Cyan)
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s🏠 Hostname:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Hostname)
|
|||
|
|
fmt.Printf("%s%s💻 OS:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.OS)
|
|||
|
|
fmt.Printf("%s%s🔧 Platform:%s %s\n", colors.Blue, colors.Bold, colors.Reset, stats.Platform)
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
PrintSectionHeader("CPU")
|
|||
|
|
fmt.Printf("%s%s🖥️ Model:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.CPUModel)
|
|||
|
|
fmt.Printf("%s%s🔢 Cores:%s %d\n", colors.Green, colors.Bold, colors.Reset, stats.CPUCount)
|
|||
|
|
fmt.Printf("%s%s📊 Load Avg:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.LoadAvg)
|
|||
|
|
fmt.Printf("%s%s⏱️ Uptime:%s %s\n", colors.Green, colors.Bold, colors.Reset, stats.Uptime)
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
PrintSectionHeader("MEMORY")
|
|||
|
|
memTotal := formatter.FormatSize(stats.MemTotal)
|
|||
|
|
memUsed := formatter.FormatSize(stats.MemUsed)
|
|||
|
|
memFree := formatter.FormatSize(stats.MemFree)
|
|||
|
|
memPct := float64(stats.MemUsed) / float64(stats.MemTotal) * 100
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s💾 Total:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memTotal)
|
|||
|
|
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Purple, colors.Bold, colors.Reset, memUsed, memPct)
|
|||
|
|
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Purple, colors.Bold, colors.Reset, memFree)
|
|||
|
|
printMemoryBar(stats.MemUsed, stats.MemTotal)
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
PrintSectionHeader("DISK")
|
|||
|
|
diskTotal := formatter.FormatSize(stats.DiskTotal)
|
|||
|
|
diskUsed := formatter.FormatSize(stats.DiskUsed)
|
|||
|
|
diskFree := formatter.FormatSize(stats.DiskFree)
|
|||
|
|
diskPct := float64(stats.DiskUsed) / float64(stats.DiskTotal) * 100
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%s💽 Total:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskTotal)
|
|||
|
|
fmt.Printf("%s%s📈 Used:%s %s (%.1f%%)\n", colors.Yellow, colors.Bold, colors.Reset, diskUsed, diskPct)
|
|||
|
|
fmt.Printf("%s%s📉 Free:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, diskFree)
|
|||
|
|
printDiskBar(stats.DiskUsed, stats.DiskTotal)
|
|||
|
|
fmt.Println()
|
|||
|
|
|
|||
|
|
if len(stats.TopProcs) > 0 {
|
|||
|
|
PrintSectionHeader("TOP PROCESSES")
|
|||
|
|
headers := []string{"PID", "Name", "CPU%", "MEM%"}
|
|||
|
|
data := make([][]string, 0, len(stats.TopProcs))
|
|||
|
|
|
|||
|
|
for _, proc := range stats.TopProcs {
|
|||
|
|
row := []string{
|
|||
|
|
fmt.Sprintf("%d", proc.PID),
|
|||
|
|
truncateString(proc.Name, 20),
|
|||
|
|
fmt.Sprintf("%.1f", proc.CPU),
|
|||
|
|
fmt.Sprintf("%.1f", proc.Mem),
|
|||
|
|
}
|
|||
|
|
data = append(data, row)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
formatter.PrintTable(headers, data, colors.Cyan)
|
|||
|
|
fmt.Println()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func printMemoryBar(used, total int64) {
|
|||
|
|
width := 30
|
|||
|
|
if total == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
fillLen := int(float64(used) / float64(total) * float64(width))
|
|||
|
|
|
|||
|
|
bar := "["
|
|||
|
|
for i := 0; i < width; i++ {
|
|||
|
|
if i < fillLen {
|
|||
|
|
if i < width/3 {
|
|||
|
|
bar += colors.Green + "█" + colors.Reset
|
|||
|
|
} else if i < 2*width/3 {
|
|||
|
|
bar += colors.Yellow + "█" + colors.Reset
|
|||
|
|
} else {
|
|||
|
|
bar += colors.Red + "█" + colors.Reset
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
bar += colors.Gray + "░" + colors.Reset
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
bar += "]"
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%sMemory Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func printDiskBar(used, total int64) {
|
|||
|
|
width := 30
|
|||
|
|
if total == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
fillLen := int(float64(used) / float64(total) * float64(width))
|
|||
|
|
|
|||
|
|
bar := "["
|
|||
|
|
for i := 0; i < width; i++ {
|
|||
|
|
if i < fillLen {
|
|||
|
|
if i < width/3 {
|
|||
|
|
bar += colors.Green + "█" + colors.Reset
|
|||
|
|
} else if i < 2*width/3 {
|
|||
|
|
bar += colors.Yellow + "█" + colors.Reset
|
|||
|
|
} else {
|
|||
|
|
bar += colors.Red + "█" + colors.Reset
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
bar += colors.Gray + "░" + colors.Reset
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
bar += "]"
|
|||
|
|
|
|||
|
|
fmt.Printf("%s%sDisk Bar:%s %s\n", colors.Gray, colors.Bold, colors.Reset, bar)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getSystemStats() (*SystemStats, error) {
|
|||
|
|
stats := &SystemStats{}
|
|||
|
|
|
|||
|
|
stats.Hostname, _ = os.Hostname()
|
|||
|
|
stats.OS = runtime.GOOS
|
|||
|
|
stats.Platform = runtime.GOARCH
|
|||
|
|
|
|||
|
|
stats.CPUModel = getCPUModel()
|
|||
|
|
stats.CPUCount = runtime.NumCPU()
|
|||
|
|
|
|||
|
|
stats.Uptime = getUptime()
|
|||
|
|
stats.LoadAvg = getLoadAvg()
|
|||
|
|
|
|||
|
|
getMemoryStats(stats)
|
|||
|
|
getDiskStats(stats)
|
|||
|
|
|
|||
|
|
stats.TopProcs = getTopProcs(5)
|
|||
|
|
|
|||
|
|
return stats, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getCPUModel() string {
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
|||
|
|
lines := strings.Split(string(data), "\n")
|
|||
|
|
for _, line := range lines {
|
|||
|
|
if strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Processor") || strings.HasPrefix(line, "cpu model") {
|
|||
|
|
parts := strings.SplitN(line, ":", 2)
|
|||
|
|
if len(parts) == 2 {
|
|||
|
|
return strings.TrimSpace(parts[1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "Unknown"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getUptime() string {
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
if data, err := os.ReadFile("/proc/uptime"); err == nil {
|
|||
|
|
parts := strings.Split(string(data), " ")
|
|||
|
|
if len(parts) >= 1 {
|
|||
|
|
if secs, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64); err == nil {
|
|||
|
|
duration := time.Duration(int64(secs)) * time.Second
|
|||
|
|
return formatDuration(duration)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "Unknown"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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("%dd %dh %dm", days, hours, minutes)
|
|||
|
|
}
|
|||
|
|
if hours > 0 {
|
|||
|
|
return fmt.Sprintf("%dh %dm", hours, minutes)
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%dm", minutes)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getLoadAvg() string {
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
if data, err := os.ReadFile("/proc/loadavg"); err == nil {
|
|||
|
|
parts := strings.Split(strings.TrimSpace(string(data)), " ")
|
|||
|
|
if len(parts) >= 3 {
|
|||
|
|
return fmt.Sprintf("%.2f %.2f %.2f",
|
|||
|
|
parseFloatSafe(parts[0]),
|
|||
|
|
parseFloatSafe(parts[1]),
|
|||
|
|
parseFloatSafe(parts[2]))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "N/A"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseFloatSafe(s string) float64 {
|
|||
|
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|||
|
|
return f
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getMemoryStats(stats *SystemStats) {
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
|||
|
|
lines := strings.Split(string(data), "\n")
|
|||
|
|
for _, line := range lines {
|
|||
|
|
if strings.HasPrefix(line, "MemTotal") {
|
|||
|
|
stats.MemTotal = parseMeminfoLine(line)
|
|||
|
|
} else if strings.HasPrefix(line, "MemAvailable") || strings.HasPrefix(line, "MemFree") {
|
|||
|
|
if stats.MemFree == 0 {
|
|||
|
|
stats.MemFree = parseMeminfoLine(line)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if stats.MemFree > 0 && stats.MemTotal > 0 {
|
|||
|
|
stats.MemUsed = stats.MemTotal - stats.MemFree
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseMeminfoLine(line string) int64 {
|
|||
|
|
parts := strings.Fields(line)
|
|||
|
|
if len(parts) >= 2 {
|
|||
|
|
if val, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
|||
|
|
return val * 1024
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getDiskStats(stats *SystemStats) {
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
output, err := exec.Command("df", "-k", "/").Output()
|
|||
|
|
if err == nil {
|
|||
|
|
lines := strings.Split(string(output), "\n")
|
|||
|
|
if len(lines) >= 2 {
|
|||
|
|
parts := strings.Fields(lines[1])
|
|||
|
|
if len(parts) >= 4 {
|
|||
|
|
stats.DiskTotal, _ = strconv.ParseInt(parts[1], 10, 64)
|
|||
|
|
stats.DiskUsed, _ = strconv.ParseInt(parts[2], 10, 64)
|
|||
|
|
stats.DiskFree, _ = strconv.ParseInt(parts[3], 10, 64)
|
|||
|
|
stats.DiskTotal *= 1024
|
|||
|
|
stats.DiskUsed *= 1024
|
|||
|
|
stats.DiskFree *= 1024
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func getTopProcs(count int) []ProcInfo {
|
|||
|
|
var procs []ProcInfo
|
|||
|
|
|
|||
|
|
if runtime.GOOS == "linux" {
|
|||
|
|
dir, err := os.Open("/proc")
|
|||
|
|
if err != nil {
|
|||
|
|
return procs
|
|||
|
|
}
|
|||
|
|
defer dir.Close()
|
|||
|
|
|
|||
|
|
entries, err := dir.Readdirnames(count * 3)
|
|||
|
|
if err != nil {
|
|||
|
|
return procs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, entry := range entries {
|
|||
|
|
pid, err := strconv.Atoi(entry)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stat, err := readProcStat(pid)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
procs = append(procs, stat)
|
|||
|
|
if len(procs) >= count {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return procs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readProcStat(pid int) (ProcInfo, error) {
|
|||
|
|
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
|||
|
|
if err != nil {
|
|||
|
|
return ProcInfo{}, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
parts := strings.SplitN(string(data), " ", 4)
|
|||
|
|
if len(parts) < 4 {
|
|||
|
|
return ProcInfo{}, fmt.Errorf("invalid format")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
name := parts[1]
|
|||
|
|
name = strings.Trim(name, "(")
|
|||
|
|
name = strings.TrimRight(name, ")")
|
|||
|
|
|
|||
|
|
comm, _ := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
|||
|
|
commName := strings.TrimSpace(string(comm))
|
|||
|
|
|
|||
|
|
var rusage syscall.Rusage
|
|||
|
|
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
|||
|
|
|
|||
|
|
return ProcInfo{
|
|||
|
|
PID: pid,
|
|||
|
|
Name: commName,
|
|||
|
|
CPU: 0,
|
|||
|
|
Mem: 0,
|
|||
|
|
Command: name,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func truncateString(s string, maxLen int) string {
|
|||
|
|
if len(s) <= maxLen {
|
|||
|
|
return s
|
|||
|
|
}
|
|||
|
|
return s[:maxLen-3] + "..."
|
|||
|
|
}
|