diff options
Diffstat (limited to 'internal')
| -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 |
7 files changed, 178 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 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() |
