diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | MACOS.md | 71 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | internal/collector/collector.go | 19 | ||||
| -rw-r--r-- | internal/collector/loadbars-remote-darwin.sh | 77 | ||||
| -rw-r--r-- | internal/collector/loadbars-remote.sh | 35 | ||||
| -rw-r--r-- | internal/collector/script.go | 13 | ||||
| -rw-r--r-- | internal/display/activate.go | 8 | ||||
| -rw-r--r-- | internal/display/activate_darwin.go | 24 | ||||
| -rw-r--r-- | internal/display/display.go | 3 |
10 files changed, 263 insertions, 3 deletions
@@ -1,2 +1,3 @@ .serena/ /loadbars +.claude/ diff --git a/MACOS.md b/MACOS.md new file mode 100644 index 0000000..ba6efda --- /dev/null +++ b/MACOS.md @@ -0,0 +1,71 @@ +# macOS Support for Loadbars + +## What was implemented + +Loadbars now fully supports macOS with automatic window activation built into the binary. + +### Changes made: + +1. **Script embedding** - Both Linux and Darwin monitoring scripts are embedded in the binary +2. **OS detection** - Automatically uses the correct script based on the host: + - `localhost` on macOS → Darwin script (sysctl, vm_stat, netstat, iostat) + - `localhost` on Linux → Linux script (/proc filesystem) + - All remote hosts → Linux script (assumes remote servers are Linux) +3. **Window activation** - macOS-specific code automatically brings SDL window to foreground + - Uses build tags (`activate_darwin.go` and `activate.go`) + - No external helper script needed + +## Usage on macOS + +Simply run the binary directly: + +```bash +# Monitor localhost +./loadbars --showcores --showmem --shownet + +# Monitor remote Linux servers +./loadbars server1.example.com server2.example.com --showcores + +# Monitor both localhost and remotes +./loadbars localhost server1.example.com server2.example.com --showcores +``` + +The window will automatically appear in the foreground. + +## Building on macOS + +Requirements: +```bash +brew install sdl2 +``` + +Build: +```bash +mage build +# or +go build -o loadbars ./cmd/loadbars +``` + +## Technical details + +### Script selection logic +- `internal/collector/script.go` - Embeds both scripts +- `internal/collector/collector.go` - Selects script based on host: + - For localhost: checks if `/proc` exists to determine Linux vs macOS + - For remote hosts: always uses Linux script + +### Window activation +- `internal/display/activate_darwin.go` - macOS-specific activation using `open -a` +- `internal/display/activate.go` - No-op for other platforms +- Called automatically after SDL window creation + +### Darwin monitoring script +- Uses macOS native tools: `sysctl`, `vm_stat`, `netstat -ibn`, `iostat` +- Outputs same protocol format as Linux script (M LOADAVG, M MEMSTATS, etc.) +- Limitations: No per-core CPU stats on macOS (iostat limitation) + +## Known limitations + +- Remote macOS hosts are not supported (assumes all remote hosts are Linux) +- macOS script doesn't provide per-core CPU statistics (iostat limitation) +- Swap usage on macOS always shows 0 (macOS uses compressed memory differently) @@ -10,8 +10,11 @@ loadbars [LIST OF HOSTNAMES] [OPTIONS] ### Tested platforms -This version of loadbars has been tested on Fedora Linux 43 and should work on -most modern Linux distributions (RHEL, CentOS, Ubuntu, Debian, etc.). +This version of loadbars has been tested on: +- Fedora Linux 43 and most modern Linux distributions (RHEL, CentOS, Ubuntu, Debian, etc.) +- macOS (Darwin) - localhost monitoring uses native macOS tools (sysctl, vm_stat, netstat, iostat) + +**Note:** Remote hosts are assumed to be Linux (using /proc filesystem). When running on macOS, localhost monitoring uses macOS-specific commands, while remote hosts use the Linux script. ### I like flying elephants @@ -102,6 +105,14 @@ On Ubuntu/Debian: sudo apt install libsdl2-dev ``` +On macOS: + +```bash +brew install sdl2 +``` + +**macOS Note:** The window will automatically come to the foreground when launched. No additional scripts needed. + ### Running from Source To run loadbars directly from the source directory: diff --git a/internal/collector/collector.go b/internal/collector/collector.go index dea88c7..cab9df0 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -25,7 +25,14 @@ type StatsStore interface { // The script is embedded in the binary; no external script file is required. func Run(ctx context.Context, host string, cfg *config.Config, store StatsStore) error { hostKey, user := splitHostUser(host) - script := bytes.NewReader(RemoteScript) + + // Select script: Darwin for localhost on macOS, Linux for everything else (all remotes are Linux) + scriptBytes := LinuxScript + if isLocal(hostKey) { + scriptBytes = getLocalScript() + } + + script := bytes.NewReader(scriptBytes) var scanner *bufio.Scanner if isLocal(hostKey) { cmd := exec.CommandContext(ctx, "bash", "-s") @@ -121,3 +128,13 @@ func splitHostUser(host string) (h, u string) { func isLocal(h string) bool { return h == "localhost" || h == "127.0.0.1" } + +// getLocalScript returns the appropriate script for the local OS +func getLocalScript() []byte { + // Check if /proc exists (Linux/Unix) + if _, err := exec.Command("test", "-d", "/proc").CombinedOutput(); err == nil { + return LinuxScript + } + // Otherwise assume macOS + return DarwinScript +} diff --git a/internal/collector/loadbars-remote-darwin.sh b/internal/collector/loadbars-remote-darwin.sh new file mode 100644 index 0000000..f82c802 --- /dev/null +++ b/internal/collector/loadbars-remote-darwin.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# loadbars-remote-darwin.sh - macOS version using sysctl, vm_stat, netstat, and iostat +# Emits loadbars protocol (M LOADAVG, M MEMSTATS, M NETSTATS, M CPUSTATS) +# Interval for CPU sampling (seconds) +INTERVAL=0.14 + +# Get number of CPUs +NCPU=$(sysctl -n hw.ncpu) + +while true; do + # Load average: from sysctl + echo "M LOADAVG" + sysctl -n vm.loadavg 2>/dev/null | awk '{print $2";"$3";"$4}' || echo "0;0;0" + + # Memory: convert vm_stat output to /proc/meminfo-like format + echo "M MEMSTATS" + vm_stat 2>/dev/null | awk ' + BEGIN { pagesize = 4096 } + /page size of ([0-9]+)/ { pagesize = $8 } + /Pages free:/ { free = $3 * pagesize / 1024 } + /Pages active:/ { active = $3 * pagesize / 1024 } + /Pages inactive:/ { inactive = $3 * pagesize / 1024 } + /Pages speculative:/ { speculative = $3 * pagesize / 1024 } + /Pages wired down:/ { wired = $4 * pagesize / 1024 } + /Pages occupied by compressor:/ { compressed = $5 * pagesize / 1024 } + END { + total = free + active + inactive + speculative + wired + compressed + printf "MemTotal: %d kB\n", total + printf "MemFree: %d kB\n", free + printf "MemAvailable: %d kB\n", free + inactive + speculative + printf "SwapTotal: 0 kB\n" + printf "SwapFree: 0 kB\n" + } + ' + + # Network: use netstat -ibn for interface stats + echo "M NETSTATS" + netstat -ibn 2>/dev/null | awk ' + NR > 1 && $1 !~ /^Name/ && $3 ~ /^<Link/ { + # netstat -ibn output on macOS: + # Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll + iface = $1 + ipkts = $5 + ierrs = $6 + ibytes = $7 + opkts = $8 + oerrs = $9 + obytes = $10 + + if (ibytes ~ /^[0-9]+$/ && obytes ~ /^[0-9]+$/) { + printf "%s:b=%s;tb=%s;p=%s;tp=%s e=%s;te=%s;d=0;td=0\n", + iface, ibytes, obytes, ipkts, opkts, ierrs, oerrs + } + } + ' + + # CPU: macOS doesn't have /proc/stat, use iostat for CPU percentages + echo "M CPUSTATS" + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + # iostat output on macOS: user sys idle + # Convert to /proc/stat format: cpu user nice system idle iowait irq softirq steal guest guest_nice + # We'll simulate values since macOS doesn't provide all fields + iostat -c 1 2>/dev/null | tail -1 | awk -v ncpu="$NCPU" ' + { + # iostat columns: %user %nice %sys %idle (approximately) + # Multiply by ncpu to get total ticks (simulated) + user = int($3 * ncpu * 10) + sys = int($4 * ncpu * 10) + idle = int($5 * ncpu * 10) + + # Output in /proc/stat format + printf "cpu %d 0 %d %d 0 0 0 0 0 0\n", user, sys, idle + } + ' + sleep "$INTERVAL" 2>/dev/null || true + done +done diff --git a/internal/collector/loadbars-remote.sh b/internal/collector/loadbars-remote.sh new file mode 100644 index 0000000..9037ad8 --- /dev/null +++ b/internal/collector/loadbars-remote.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# loadbars-remote.sh - Emits loadbars protocol (M LOADAVG, M MEMSTATS, M NETSTATS, M CPUSTATS) +# for local or remote execution. No Perl required. +# Usage: bash loadbars-remote.sh +# Interval for CPU sampling (seconds) +INTERVAL=0.14 + +while true; do + # Load average: first 3 fields of /proc/loadavg joined by ; + echo "M LOADAVG" + read -r l1 l5 l15 _ < /proc/loadavg 2>/dev/null || true + echo "${l1:-0};${l5:-0};${l15:-0}" + + # Memory: full /proc/meminfo + echo "M MEMSTATS" + cat /proc/meminfo 2>/dev/null || true + + # Network: /proc/net/dev, skip 2 header lines, then "iface: rx... tx..." + echo "M NETSTATS" + while IFS= read -r line; do + line="${line/:/ }" + set -- $line + # $1=iface, $2=rx_bytes $3=rx_packets $4=rx_errs $5=rx_drop ... $10=tx_bytes $11=tx_packets $12=tx_errs $13=tx_drop + if [ -n "$2" ] || [ -n "${10:-}" ]; then + echo "$1:b=${2:-0};tb=${10:-0};p=${3:-0};tp=${11:-0} e=${4:-0};te=${12:-0};d=${5:-0};td=${13:-0}" + fi + done < <(tail -n +3 /proc/net/dev 2>/dev/null) + + # CPU: /proc/stat, 20 times with INTERVAL sleep + echo "M CPUSTATS" + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + cat /proc/stat 2>/dev/null || true + sleep "$INTERVAL" 2>/dev/null || true + done +done diff --git a/internal/collector/script.go b/internal/collector/script.go new file mode 100644 index 0000000..3be2190 --- /dev/null +++ b/internal/collector/script.go @@ -0,0 +1,13 @@ +package collector + +import _ "embed" + +// LinuxScript contains the embedded loadbars-remote.sh script for Linux hosts +// +//go:embed loadbars-remote.sh +var LinuxScript []byte + +// DarwinScript contains the embedded loadbars-remote-darwin.sh script for macOS hosts +// +//go:embed loadbars-remote-darwin.sh +var DarwinScript []byte diff --git a/internal/display/activate.go b/internal/display/activate.go new file mode 100644 index 0000000..b9040d7 --- /dev/null +++ b/internal/display/activate.go @@ -0,0 +1,8 @@ +// +build !darwin + +package display + +// activateWindow is a no-op on non-macOS platforms +func activateWindow() { + // Nothing needed on Linux/other platforms +} diff --git a/internal/display/activate_darwin.go b/internal/display/activate_darwin.go new file mode 100644 index 0000000..54c6b94 --- /dev/null +++ b/internal/display/activate_darwin.go @@ -0,0 +1,24 @@ +// +build darwin + +package display + +import ( + "os" + "os/exec" + "time" +) + +// activateWindow brings the SDL window to the foreground on macOS +func activateWindow() { + // Give SDL a moment to create the window + go func() { + time.Sleep(500 * time.Millisecond) + // Get the executable path + execPath, err := os.Executable() + if err != nil { + return + } + // Use open -a to bring the window to foreground + exec.Command("open", "-a", execPath).Run() + }() +} diff --git a/internal/display/display.go b/internal/display/display.go index 231b8c6..10a0b04 100644 --- a/internal/display/display.go +++ b/internal/display/display.go @@ -87,6 +87,9 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error { defer renderer.Destroy() window.SetTitle(title) + // On macOS, bring the window to the foreground + activateWindow() + state := newRunState(cfg, int32(width), int32(height)) ticker := time.NewTicker(time.Duration(constants.IntervalSDL * float64(time.Second))) defer ticker.Stop() |
