package system import ( "bufio" "fmt" "os" "os/exec" "strconv" "strings" "time" ) // ── Types ─────────────────────────────────────────────────────────── type CoreUsage struct { Core int `json:"core"` User float64 `json:"user"` System float64 `json:"system"` Idle float64 `json:"idle"` IOWait float64 `json:"iowait"` Percent float64 `json:"percent"` } type CPUInfo struct { Overall float64 `json:"overall"` Idle float64 `json:"idle"` Cores []CoreUsage `json:"cores"` } type MemInfo struct { TotalBytes uint64 `json:"total_bytes"` UsedBytes uint64 `json:"used_bytes"` FreeBytes uint64 `json:"free_bytes"` AvailableBytes uint64 `json:"available_bytes"` BuffersBytes uint64 `json:"buffers_bytes"` CachedBytes uint64 `json:"cached_bytes"` SwapTotalBytes uint64 `json:"swap_total_bytes"` SwapUsedBytes uint64 `json:"swap_used_bytes"` SwapFreeBytes uint64 `json:"swap_free_bytes"` Total string `json:"total"` Used string `json:"used"` Free string `json:"free"` Available string `json:"available"` Buffers string `json:"buffers"` Cached string `json:"cached"` SwapTotal string `json:"swap_total"` SwapUsed string `json:"swap_used"` SwapFree string `json:"swap_free"` } type DiskInfo struct { Filesystem string `json:"filesystem"` Size string `json:"size"` Used string `json:"used"` Available string `json:"available"` UsePercent string `json:"use_percent"` MountPoint string `json:"mount_point"` } type NetInfo struct { Interface string `json:"interface"` RxBytes uint64 `json:"rx_bytes"` RxPackets uint64 `json:"rx_packets"` RxErrors uint64 `json:"rx_errors"` RxDropped uint64 `json:"rx_dropped"` TxBytes uint64 `json:"tx_bytes"` TxPackets uint64 `json:"tx_packets"` TxErrors uint64 `json:"tx_errors"` TxDropped uint64 `json:"tx_dropped"` RxHuman string `json:"rx_human"` TxHuman string `json:"tx_human"` } type UptimeInfo struct { Seconds float64 `json:"seconds"` IdleSeconds float64 `json:"idle_seconds"` HumanReadable string `json:"human_readable"` } type LoadInfo struct { Load1 float64 `json:"load_1"` Load5 float64 `json:"load_5"` Load15 float64 `json:"load_15"` RunningProcs int `json:"running_procs"` TotalProcs int `json:"total_procs"` } type ProcessInfo struct { PID int `json:"pid"` User string `json:"user"` CPU float64 `json:"cpu"` Mem float64 `json:"mem"` RSS int64 `json:"rss"` Command string `json:"command"` } // ── CPU ───────────────────────────────────────────────────────────── // readCPUStats reads /proc/stat and returns a map of cpu label to field slices. func readCPUStats() (map[string][]uint64, error) { f, err := os.Open("/proc/stat") if err != nil { return nil, fmt.Errorf("opening /proc/stat: %w", err) } defer f.Close() result := make(map[string][]uint64) scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "cpu") { continue } fields := strings.Fields(line) if len(fields) < 5 { continue } label := fields[0] var vals []uint64 for _, field := range fields[1:] { v, _ := strconv.ParseUint(field, 10, 64) vals = append(vals, v) } result[label] = vals } return result, scanner.Err() } // parseCPUFields converts raw jiffie counts into a CoreUsage. // Fields: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice func parseCPUFields(core int, before, after []uint64) CoreUsage { cu := CoreUsage{Core: core} if len(before) < 5 || len(after) < 5 { return cu } // Sum all fields for total jiffies var totalBefore, totalAfter uint64 for _, v := range before { totalBefore += v } for _, v := range after { totalAfter += v } totalDelta := float64(totalAfter - totalBefore) if totalDelta == 0 { return cu } userDelta := float64((after[0] + after[1]) - (before[0] + before[1])) systemDelta := float64(after[2] - before[2]) idleDelta := float64(after[3] - before[3]) var iowaitDelta float64 if len(after) > 4 && len(before) > 4 { iowaitDelta = float64(after[4] - before[4]) } cu.User = userDelta / totalDelta * 100 cu.System = systemDelta / totalDelta * 100 cu.Idle = idleDelta / totalDelta * 100 cu.IOWait = iowaitDelta / totalDelta * 100 cu.Percent = 100 - cu.Idle return cu } // GetCPUUsage samples /proc/stat twice with a brief interval to compute usage. func GetCPUUsage() (CPUInfo, error) { info := CPUInfo{} before, err := readCPUStats() if err != nil { return info, err } time.Sleep(250 * time.Millisecond) after, err := readCPUStats() if err != nil { return info, err } // Overall CPU (the "cpu" aggregate line) if bv, ok := before["cpu"]; ok { if av, ok := after["cpu"]; ok { overall := parseCPUFields(-1, bv, av) info.Overall = overall.Percent info.Idle = overall.Idle } } // Per-core for i := 0; ; i++ { label := fmt.Sprintf("cpu%d", i) bv, ok1 := before[label] av, ok2 := after[label] if !ok1 || !ok2 { break } info.Cores = append(info.Cores, parseCPUFields(i, bv, av)) } return info, nil } // ── Memory ────────────────────────────────────────────────────────── func GetMemory() (MemInfo, error) { info := MemInfo{} f, err := os.Open("/proc/meminfo") if err != nil { return info, fmt.Errorf("opening /proc/meminfo: %w", err) } defer f.Close() vals := make(map[string]uint64) scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) valStr := strings.TrimSpace(parts[1]) valStr = strings.TrimSuffix(valStr, " kB") valStr = strings.TrimSpace(valStr) v, err := strconv.ParseUint(valStr, 10, 64) if err != nil { continue } vals[key] = v * 1024 // convert kB to bytes } if err := scanner.Err(); err != nil { return info, err } info.TotalBytes = vals["MemTotal"] info.FreeBytes = vals["MemFree"] info.AvailableBytes = vals["MemAvailable"] info.BuffersBytes = vals["Buffers"] info.CachedBytes = vals["Cached"] info.SwapTotalBytes = vals["SwapTotal"] info.SwapFreeBytes = vals["SwapFree"] info.SwapUsedBytes = info.SwapTotalBytes - info.SwapFreeBytes info.UsedBytes = info.TotalBytes - info.FreeBytes - info.BuffersBytes - info.CachedBytes if info.UsedBytes > info.TotalBytes { // Overflow guard: if buffers+cached > total-free, use simpler calculation info.UsedBytes = info.TotalBytes - info.AvailableBytes } info.Total = humanBytes(info.TotalBytes) info.Used = humanBytes(info.UsedBytes) info.Free = humanBytes(info.FreeBytes) info.Available = humanBytes(info.AvailableBytes) info.Buffers = humanBytes(info.BuffersBytes) info.Cached = humanBytes(info.CachedBytes) info.SwapTotal = humanBytes(info.SwapTotalBytes) info.SwapUsed = humanBytes(info.SwapUsedBytes) info.SwapFree = humanBytes(info.SwapFreeBytes) return info, nil } // ── Disk ──────────────────────────────────────────────────────────── func GetDisk() ([]DiskInfo, error) { // Try with filesystem type filters first for real block devices out, err := exec.Command("df", "-h", "--type=ext4", "--type=xfs", "--type=btrfs", "--type=ext3").CombinedOutput() if err != nil { // Fallback: exclude pseudo filesystems out, err = exec.Command("df", "-h", "--exclude-type=tmpfs", "--exclude-type=devtmpfs", "--exclude-type=squashfs").CombinedOutput() if err != nil { // Last resort: all filesystems out, err = exec.Command("df", "-h").CombinedOutput() if err != nil { return nil, fmt.Errorf("df command failed: %w (%s)", err, string(out)) } } } var disks []DiskInfo lines := strings.Split(strings.TrimSpace(string(out)), "\n") for i, line := range lines { if i == 0 || strings.TrimSpace(line) == "" { continue // skip header } fields := strings.Fields(line) if len(fields) < 6 { continue } disks = append(disks, DiskInfo{ Filesystem: fields[0], Size: fields[1], Used: fields[2], Available: fields[3], UsePercent: fields[4], MountPoint: fields[5], }) } return disks, nil } // ── Network ───────────────────────────────────────────────────────── func GetNetwork() ([]NetInfo, error) { f, err := os.Open("/proc/net/dev") if err != nil { return nil, fmt.Errorf("opening /proc/net/dev: %w", err) } defer f.Close() var interfaces []NetInfo scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ if lineNum <= 2 { continue // skip the two header lines } line := scanner.Text() // Format: " iface: rx_bytes rx_packets rx_errs rx_drop ... tx_bytes tx_packets tx_errs tx_drop ..." colonIdx := strings.Index(line, ":") if colonIdx < 0 { continue } iface := strings.TrimSpace(line[:colonIdx]) rest := strings.TrimSpace(line[colonIdx+1:]) fields := strings.Fields(rest) if len(fields) < 10 { continue } rxBytes, _ := strconv.ParseUint(fields[0], 10, 64) rxPackets, _ := strconv.ParseUint(fields[1], 10, 64) rxErrors, _ := strconv.ParseUint(fields[2], 10, 64) rxDropped, _ := strconv.ParseUint(fields[3], 10, 64) txBytes, _ := strconv.ParseUint(fields[8], 10, 64) txPackets, _ := strconv.ParseUint(fields[9], 10, 64) txErrors, _ := strconv.ParseUint(fields[10], 10, 64) txDropped, _ := strconv.ParseUint(fields[11], 10, 64) interfaces = append(interfaces, NetInfo{ Interface: iface, RxBytes: rxBytes, RxPackets: rxPackets, RxErrors: rxErrors, RxDropped: rxDropped, TxBytes: txBytes, TxPackets: txPackets, TxErrors: txErrors, TxDropped: txDropped, RxHuman: humanBytes(rxBytes), TxHuman: humanBytes(txBytes), }) } return interfaces, scanner.Err() } // ── Uptime ────────────────────────────────────────────────────────── func GetUptime() (UptimeInfo, error) { info := UptimeInfo{} data, err := os.ReadFile("/proc/uptime") if err != nil { return info, fmt.Errorf("reading /proc/uptime: %w", err) } fields := strings.Fields(strings.TrimSpace(string(data))) if len(fields) < 2 { return info, fmt.Errorf("unexpected /proc/uptime format") } info.Seconds, _ = strconv.ParseFloat(fields[0], 64) info.IdleSeconds, _ = strconv.ParseFloat(fields[1], 64) // Build human readable string totalSec := int(info.Seconds) days := totalSec / 86400 hours := (totalSec % 86400) / 3600 minutes := (totalSec % 3600) / 60 seconds := totalSec % 60 parts := []string{} if days > 0 { parts = append(parts, fmt.Sprintf("%d day%s", days, plural(days))) } if hours > 0 { parts = append(parts, fmt.Sprintf("%d hour%s", hours, plural(hours))) } if minutes > 0 { parts = append(parts, fmt.Sprintf("%d minute%s", minutes, plural(minutes))) } if len(parts) == 0 || (days == 0 && hours == 0 && minutes == 0) { parts = append(parts, fmt.Sprintf("%d second%s", seconds, plural(seconds))) } info.HumanReadable = strings.Join(parts, ", ") return info, nil } // ── Load Average ──────────────────────────────────────────────────── func GetLoadAvg() (LoadInfo, error) { info := LoadInfo{} data, err := os.ReadFile("/proc/loadavg") if err != nil { return info, fmt.Errorf("reading /proc/loadavg: %w", err) } fields := strings.Fields(strings.TrimSpace(string(data))) if len(fields) < 4 { return info, fmt.Errorf("unexpected /proc/loadavg format") } info.Load1, _ = strconv.ParseFloat(fields[0], 64) info.Load5, _ = strconv.ParseFloat(fields[1], 64) info.Load15, _ = strconv.ParseFloat(fields[2], 64) // fields[3] is "running/total" format procParts := strings.SplitN(fields[3], "/", 2) if len(procParts) == 2 { info.RunningProcs, _ = strconv.Atoi(procParts[0]) info.TotalProcs, _ = strconv.Atoi(procParts[1]) } return info, nil } // ── Top Processes ─────────────────────────────────────────────────── func GetTopProcesses(n int) ([]ProcessInfo, error) { if n <= 0 { n = 10 } // ps aux --sort=-%mem gives us processes sorted by memory usage descending out, err := exec.Command("ps", "aux", "--sort=-%mem").CombinedOutput() if err != nil { return nil, fmt.Errorf("ps command failed: %w (%s)", err, string(out)) } var procs []ProcessInfo lines := strings.Split(strings.TrimSpace(string(out)), "\n") for i, line := range lines { if i == 0 { continue // skip header } if len(procs) >= n { break } fields := strings.Fields(line) if len(fields) < 11 { continue } pid, _ := strconv.Atoi(fields[1]) cpu, _ := strconv.ParseFloat(fields[2], 64) mem, _ := strconv.ParseFloat(fields[3], 64) rss, _ := strconv.ParseInt(fields[5], 10, 64) // Command is everything from field 10 onward (may contain spaces) command := strings.Join(fields[10:], " ") procs = append(procs, ProcessInfo{ PID: pid, User: fields[0], CPU: cpu, Mem: mem, RSS: rss, Command: command, }) } return procs, nil } // ── Helpers ───────────────────────────────────────────────────────── func humanBytes(b uint64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } suffixes := []string{"KiB", "MiB", "GiB", "TiB", "PiB"} if exp >= len(suffixes) { exp = len(suffixes) - 1 } return fmt.Sprintf("%.1f %s", float64(b)/float64(div), suffixes[exp]) } func plural(n int) string { if n == 1 { return "" } return "s" }