diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-14 12:32:54 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-14 12:33:40 +0200 |
| commit | 50733fe4ebac28136144d5b85721ee5fd0b7850a (patch) | |
| tree | 6546058f9ea41b81502e833c50ab32ef96718ec8 /internal/collector | |
| parent | 52e70e2a065da95cdfcf7d370173003d3ce395cd (diff) | |
Add macOS support with automatic window activation
This commit adds full macOS support for loadbars, allowing it to run
natively on macOS for both localhost monitoring and remote Linux hosts.
Key changes:
- Embed both Linux and Darwin monitoring scripts in the binary
- Auto-detect localhost OS and use appropriate script
- Darwin script uses native macOS tools (sysctl, vm_stat, netstat, iostat)
- Remote hosts always use Linux script (assumes /proc filesystem)
- Automatic window activation on macOS using build tags
- No external helper scripts needed
The binary now works seamlessly on macOS:
- localhost monitoring uses macOS-specific commands
- Remote Linux hosts work via SSH with Linux script
- SDL window automatically comes to foreground on macOS
- Cross-platform build with single binary for all scenarios
Technical implementation:
- internal/collector/script.go: Embeds both scripts
- internal/collector/loadbars-remote-darwin.sh: macOS monitoring
- internal/collector/loadbars-remote.sh: Linux monitoring (copied from scripts/)
- internal/display/activate_darwin.go: macOS window activation
- internal/display/activate.go: No-op for other platforms
- Updated README.md with macOS installation instructions
- Added MACOS.md with detailed macOS documentation
Diffstat (limited to 'internal/collector')
| -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 |
4 files changed, 143 insertions, 1 deletions
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 |
