feat(beacon): native C beacon dispatcher with Bash fallback (zero Python dependency)

This commit is contained in:
Kryštof Pešek
2026-11-05 21:04:35 +01:00
parent 5f4b9ca1fb
commit 01c8b36f13
5 changed files with 983 additions and 235 deletions
+3
View File
@@ -0,0 +1,3 @@
fleet-beacon-c
*.o
*.a
+31 -235
View File
@@ -1,242 +1,38 @@
#!/usr/bin/env python3
"""
fleet-beacon.py — Multi-node Realtime Telemetry Beacon
Part of Observatoř Top (otop)
#!/usr/bin/env bash
# =============================================================================
# ~/bin/fleet-beacon — Fleet Realtime Telemetry Beacon Dispatcher
# Zero-Bloat Architecture: Native C first, Bash fallback, Python avoided.
# =============================================================================
Broadcasts lightweight UDP telemetry packets (port 9850) every 1.5s.
Zero external dependencies, minimal CPU impact (< 0.05%).
"""
BIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
import os
import sys
import time
import json
import socket
import shutil
import glob
import urllib.request
# Ensure fleet environment is active
if [ -z "$FLEET_NODE" ] || [ -z "$FLEET_ARCH" ]; then
[ -f "$BIN_DIR/bin-env" ] && source "$BIN_DIR/bin-env" 2>/dev/null || true
fi
PORT = 9850
TARGETS = [
"<broadcast>",
"192.168.1.255",
"192.168.1.100", # t520 (Ethernet)
"192.168.1.102", # t520 (WiFi)
"192.168.1.104", # bazina (Debian 13)
"192.168.1.101", # boxC (RPi storage)
"192.168.1.105", # penguin (Chromebook)
"192.168.1.103", # phone (Android)
"10.39.9.255", # WireGuard VPN Broadcast
"10.39.9.1", # boxC VPN
"10.0.0.255",
"10.0.0.18",
"10.0.0.14",
"91.139.118.43",
"192.168.43.255",
"192.168.43.1",
"127.0.0.1"
]
# 1. Prefer native C compiled binary (< 1MB RAM, 0% CPU)
if [ -x "$BIN_DIR/fleet-beacon-c" ]; then
exec "$BIN_DIR/fleet-beacon-c" "$@"
fi
def get_hostname():
if os.path.exists("/data/data/com.termux") or "TERMUX_VERSION" in os.environ:
return "phone"
h = socket.gethostname().split('.')[0]
if h in ("localhost", "android"):
return "phone"
return h
# 2. Compile native C binary on the fly if compiler exists
if command -v gcc >/dev/null 2>&1 && [ -f "$BIN_DIR/src/fleet-beacon.c" ]; then
gcc -O2 -Wall -Wextra -pedantic -o "$BIN_DIR/fleet-beacon-c" "$BIN_DIR/src/fleet-beacon.c" 2>/dev/null || true
if [ -x "$BIN_DIR/fleet-beacon-c" ]; then
exec "$BIN_DIR/fleet-beacon-c" "$@"
fi
fi
def read_cpu_percent(prev_stat):
try:
with open("/proc/stat", "r") as f:
fields = [float(x) for x in f.readline().strip().split()[1:8]]
# fields: user, nice, system, idle, iowait, irq, softirq
idle = fields[3] + fields[4]
total = sum(fields)
if prev_stat is not None:
prev_idle, prev_total = prev_stat
d_idle = idle - prev_idle
d_total = total - prev_total
pct = 100.0 * (1.0 - (d_idle / max(1.0, d_total)))
return round(max(0.0, min(100.0, pct)), 1), (idle, total)
return 0.0, (idle, total)
except Exception:
return 0.0, prev_stat
# 3. Fallback to pure Bash implementation
if [ -x "$BIN_DIR/fleet-beacon.sh" ]; then
exec "$BIN_DIR/fleet-beacon.sh" "$@"
fi
def read_memory():
total_gb, used_gb = 0.0, 0.0
try:
with open("/proc/meminfo", "r") as f:
mem = {}
for line in f:
parts = line.split(":")
if len(parts) == 2:
mem[parts[0].strip()] = float(parts[1].strip().split()[0])
total_kb = mem.get("MemTotal", 0.0)
avail_kb = mem.get("MemAvailable", mem.get("MemFree", 0.0))
total_gb = round(total_kb / (1024.0 * 1024.0), 1)
used_gb = round((total_kb - avail_kb) / (1024.0 * 1024.0), 1)
except Exception:
pass
return used_gb, total_gb
# 4. Last-ditch Python fallback if neither is available
if [ -x "$BIN_DIR/fleet-beacon.py" ] && command -v python3 >/dev/null 2>&1; then
exec python3 "$BIN_DIR/fleet-beacon.py" "$@"
fi
def read_disk():
hostname = get_hostname()
check_paths = ["/"]
if hostname == "boxC":
check_paths = ["/mnt/kof17", "/"]
elif hostname == "bazina":
check_paths = ["/mnt/ssd", "/mnt/seagate", "/"]
for p in check_paths:
if os.path.exists(p):
try:
du = shutil.disk_usage(p)
free_gb = round(du.free / (1024.0 ** 3), 1)
total_gb = round(du.total / (1024.0 ** 3), 1)
return p, free_gb, total_gb
except Exception:
continue
return "/", 0.0, 0.0
def read_gpu():
# AMD GPU busy percent (sysfs)
for path in glob.glob("/sys/class/drm/card*/device/gpu_busy_percent"):
try:
with open(path, "r") as f:
val = f.read().strip()
return f"Navi10 {val}%"
except Exception:
pass
return ""
def read_temp():
for path in ["/sys/class/thermal/thermal_zone0/temp", "/sys/class/hwmon/hwmon0/temp1_input"]:
if os.path.exists(path):
try:
with open(path, "r") as f:
return int(int(f.read().strip()) / 1000)
except Exception:
pass
return 0
def read_battery():
# Probe ACPI / sysfs battery capacity
for p in glob.glob("/sys/class/power_supply/BAT*/capacity") + glob.glob("/sys/class/power_supply/*battery*/capacity") + glob.glob("/sys/class/power_supply/sbs-*/capacity"):
try:
with open(p, "r") as f:
return int(f.read().strip())
except Exception:
pass
return -1
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
hostname = get_hostname()
prev_stat = None
print(f"[fleet-beacon] Starting telemetry beacon on {hostname} (port {PORT})...")
while True:
cpu_pct, prev_stat = read_cpu_percent(prev_stat)
mem_used, mem_total = read_memory()
mount, disk_free, disk_total = read_disk()
gpu_str = read_gpu()
temp_c = read_temp()
bat_pct = read_battery()
alert = ""
if hostname == "boxC" and mount == "/mnt/kof17" and disk_free < 3.0:
alert = f"kof17 CRITICAL: {disk_free}G free"
elif hostname in ("penguin", "crostini") and disk_free < 1.0:
alert = f"crostini LOW DISK: {disk_free}G free"
alert = f"penguin LOW DISK: {disk_free}G free"
elif bat_pct >= 0 and bat_pct <= 15:
alert = f"{hostname} BATTERY CRITICAL: {bat_pct}%"
payload = {
"name": hostname,
"hostname": hostname,
"node": hostname,
"arch": os.uname().machine,
"sign": f"[{hostname}:{os.uname().machine}]",
"timestamp": time.time(),
"cpu_percent": cpu_pct,
"mem_used_gb": mem_used,
"mem_total_gb": mem_total,
"disk_mount": mount,
"disk_free_gb": disk_free,
"disk_total_gb": disk_total,
"gpu_str": gpu_str,
"temp_c": temp_c,
"battery_percent": bat_pct,
"alert": alert
}
msg = json.dumps(payload).encode("utf-8")
for target in TARGETS:
try:
sock.sendto(msg, (target, PORT))
except Exception:
pass
# Update local SHM cache
try:
shm_fleet = "/dev/shm/fleet_telemetry.json"
data = {}
if os.path.exists(shm_fleet):
try:
with open(shm_fleet, "r") as f:
data = json.load(f)
except Exception:
data = {}
data[hostname] = payload
# Merge any WAN telemetry files (< 30s old)
try:
import glob
for wpath in glob.glob("/dev/shm/fleet_wan_*.json"):
if time.time() - os.path.getmtime(wpath) < 30:
with open(wpath, "r") as wf:
wpayload = json.load(wf)
if isinstance(wpayload, dict) and "name" in wpayload:
data[wpayload["name"]] = wpayload
except Exception:
pass
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(data, f)
try:
os.chmod(tmp_path, 0o666)
except Exception:
pass
os.replace(tmp_path, shm_fleet)
except Exception:
pass
# WAN HTTP bridge fallback for mobile/remote nodes (e.g. Staroměstská)
if hostname != "boxC":
try:
req = urllib.request.Request(
"http://91.139.118.43/telemetry.php",
data=msg,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=1.5) as resp:
if resp.status == 200:
remote_data = json.loads(resp.read().decode("utf-8"))
if isinstance(remote_data, dict) and "boxC" in remote_data:
shm_fleet = "/dev/shm/fleet_telemetry.json"
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(remote_data, f)
os.replace(tmp_path, shm_fleet)
except Exception:
pass
time.sleep(1.5)
if __name__ == "__main__":
main()
echo "Error: No viable fleet-beacon runtime found." >&2
exit 1
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""
fleet-beacon.py — Multi-node Realtime Telemetry Beacon
Part of Observatoř Top (otop)
Broadcasts lightweight UDP telemetry packets (port 9850) every 1.5s.
Zero external dependencies, minimal CPU impact (< 0.05%).
"""
import os
import sys
import time
import json
import socket
import shutil
import glob
import urllib.request
PORT = 9850
TARGETS = [
"<broadcast>",
"192.168.1.255",
"192.168.1.100", # t520 (Ethernet)
"192.168.1.102", # t520 (WiFi)
"192.168.1.104", # bazina (Debian 13)
"192.168.1.101", # boxC (RPi storage)
"192.168.1.105", # penguin (Chromebook)
"192.168.1.103", # phone (Android)
"10.39.9.255", # WireGuard VPN Broadcast
"10.39.9.1", # boxC VPN
"10.0.0.255",
"10.0.0.18",
"10.0.0.14",
"91.139.118.43",
"192.168.43.255",
"192.168.43.1",
"127.0.0.1"
]
def get_hostname():
if os.path.exists("/data/data/com.termux") or "TERMUX_VERSION" in os.environ:
return "phone"
h = socket.gethostname().split('.')[0]
if h in ("localhost", "android"):
return "phone"
return h
def read_cpu_percent(prev_stat):
try:
with open("/proc/stat", "r") as f:
fields = [float(x) for x in f.readline().strip().split()[1:8]]
# fields: user, nice, system, idle, iowait, irq, softirq
idle = fields[3] + fields[4]
total = sum(fields)
if prev_stat is not None:
prev_idle, prev_total = prev_stat
d_idle = idle - prev_idle
d_total = total - prev_total
pct = 100.0 * (1.0 - (d_idle / max(1.0, d_total)))
return round(max(0.0, min(100.0, pct)), 1), (idle, total)
return 0.0, (idle, total)
except Exception:
return 0.0, prev_stat
def read_memory():
total_gb, used_gb = 0.0, 0.0
try:
with open("/proc/meminfo", "r") as f:
mem = {}
for line in f:
parts = line.split(":")
if len(parts) == 2:
mem[parts[0].strip()] = float(parts[1].strip().split()[0])
total_kb = mem.get("MemTotal", 0.0)
avail_kb = mem.get("MemAvailable", mem.get("MemFree", 0.0))
total_gb = round(total_kb / (1024.0 * 1024.0), 1)
used_gb = round((total_kb - avail_kb) / (1024.0 * 1024.0), 1)
except Exception:
pass
return used_gb, total_gb
def read_disk():
hostname = get_hostname()
check_paths = ["/"]
if hostname == "boxC":
check_paths = ["/mnt/kof17", "/"]
elif hostname == "bazina":
check_paths = ["/mnt/ssd", "/mnt/seagate", "/"]
for p in check_paths:
if os.path.exists(p):
try:
du = shutil.disk_usage(p)
free_gb = round(du.free / (1024.0 ** 3), 1)
total_gb = round(du.total / (1024.0 ** 3), 1)
return p, free_gb, total_gb
except Exception:
continue
return "/", 0.0, 0.0
def read_gpu():
# AMD GPU busy percent (sysfs)
for path in glob.glob("/sys/class/drm/card*/device/gpu_busy_percent"):
try:
with open(path, "r") as f:
val = f.read().strip()
return f"Navi10 {val}%"
except Exception:
pass
return ""
def read_temp():
for path in ["/sys/class/thermal/thermal_zone0/temp", "/sys/class/hwmon/hwmon0/temp1_input"]:
if os.path.exists(path):
try:
with open(path, "r") as f:
return int(int(f.read().strip()) / 1000)
except Exception:
pass
return 0
def read_battery():
# Probe ACPI / sysfs battery capacity
for p in glob.glob("/sys/class/power_supply/BAT*/capacity") + glob.glob("/sys/class/power_supply/*battery*/capacity") + glob.glob("/sys/class/power_supply/sbs-*/capacity"):
try:
with open(p, "r") as f:
return int(f.read().strip())
except Exception:
pass
return -1
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
hostname = get_hostname()
prev_stat = None
print(f"[fleet-beacon] Starting telemetry beacon on {hostname} (port {PORT})...")
while True:
cpu_pct, prev_stat = read_cpu_percent(prev_stat)
mem_used, mem_total = read_memory()
mount, disk_free, disk_total = read_disk()
gpu_str = read_gpu()
temp_c = read_temp()
bat_pct = read_battery()
alert = ""
if hostname == "boxC" and mount == "/mnt/kof17" and disk_free < 3.0:
alert = f"kof17 CRITICAL: {disk_free}G free"
elif hostname in ("penguin", "crostini") and disk_free < 1.0:
alert = f"crostini LOW DISK: {disk_free}G free"
alert = f"penguin LOW DISK: {disk_free}G free"
elif bat_pct >= 0 and bat_pct <= 15:
alert = f"{hostname} BATTERY CRITICAL: {bat_pct}%"
payload = {
"name": hostname,
"hostname": hostname,
"node": hostname,
"arch": os.uname().machine,
"sign": f"[{hostname}:{os.uname().machine}]",
"timestamp": time.time(),
"cpu_percent": cpu_pct,
"mem_used_gb": mem_used,
"mem_total_gb": mem_total,
"disk_mount": mount,
"disk_free_gb": disk_free,
"disk_total_gb": disk_total,
"gpu_str": gpu_str,
"temp_c": temp_c,
"battery_percent": bat_pct,
"alert": alert
}
msg = json.dumps(payload).encode("utf-8")
for target in TARGETS:
try:
sock.sendto(msg, (target, PORT))
except Exception:
pass
# Update local SHM cache
try:
shm_fleet = "/dev/shm/fleet_telemetry.json"
data = {}
if os.path.exists(shm_fleet):
try:
with open(shm_fleet, "r") as f:
data = json.load(f)
except Exception:
data = {}
data[hostname] = payload
# Merge any WAN telemetry files (< 30s old)
try:
import glob
for wpath in glob.glob("/dev/shm/fleet_wan_*.json"):
if time.time() - os.path.getmtime(wpath) < 30:
with open(wpath, "r") as wf:
wpayload = json.load(wf)
if isinstance(wpayload, dict) and "name" in wpayload:
data[wpayload["name"]] = wpayload
except Exception:
pass
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(data, f)
try:
os.chmod(tmp_path, 0o666)
except Exception:
pass
os.replace(tmp_path, shm_fleet)
except Exception:
pass
# WAN HTTP bridge fallback for mobile/remote nodes (e.g. Staroměstská)
if hostname != "boxC":
try:
req = urllib.request.Request(
"http://91.139.118.43/telemetry.php",
data=msg,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=1.5) as resp:
if resp.status == 200:
remote_data = json.loads(resp.read().decode("utf-8"))
if isinstance(remote_data, dict) and "boxC" in remote_data:
shm_fleet = "/dev/shm/fleet_telemetry.json"
tmp_path = f"{shm_fleet}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(remote_data, f)
os.replace(tmp_path, shm_fleet)
except Exception:
pass
time.sleep(1.5)
if __name__ == "__main__":
main()
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
# =============================================================================
# fleet-beacon.sh — Multi-node Realtime Telemetry Beacon (Pure Bash Fallback)
# Observatoř Telemetry Platform // Fleet Operations
#
# Broadcasts lightweight UDP telemetry packets (port 9850) every 1.5s.
# Zero external dependencies, pure coreutils + bash.
# =============================================================================
set -e
PORT=9850
INTERVAL=1.5
TARGETS=(
"255.255.255.255"
"192.168.1.255"
"192.168.1.100"
"192.168.1.102"
"192.168.1.104"
"192.168.1.101"
"192.168.1.105"
"192.168.1.103"
"10.39.9.255"
"10.39.9.1"
"10.0.0.255"
"10.0.0.18"
"10.0.0.14"
"91.139.118.43"
"192.168.43.255"
"192.168.43.1"
"127.0.0.1"
)
# Identity resolution
if [ -z "$FLEET_NODE" ] || [ -z "$FLEET_ARCH" ]; then
[ -f "/etc/fleet_id" ] && source "/etc/fleet_id" 2>/dev/null || true
[ -f "$HOME/.fleet_id" ] && source "$HOME/.fleet_id" 2>/dev/null || true
fi
RAW_HOST="$(hostname -s 2>/dev/null || uname -n)"
if [ -z "$FLEET_NODE" ]; then
if [ -d "/data/data/com.termux" ] || [ -n "$TERMUX_VERSION" ] || [ "$RAW_HOST" = "localhost" ] || [ "$RAW_HOST" = "android" ]; then
FLEET_NODE="android"
elif [ "$RAW_HOST" = "penguin" ]; then
FLEET_NODE="crostini"
else
FLEET_NODE="$RAW_HOST"
fi
fi
FLEET_ARCH="${FLEET_ARCH:-$(uname -m)}"
FLEET_SIGN="${FLEET_SIGN:-[${FLEET_NODE}:${FLEET_ARCH}]}"
RUN_ONCE=0
PRINT_JSON=0
for arg in "$@"; do
case "$arg" in
--once|-1) RUN_ONCE=1 ;;
--json|-j) RUN_ONCE=1; PRINT_JSON=1 ;;
esac
done
if [ "$PRINT_JSON" -eq 0 ]; then
echo "[fleet-beacon-sh] Starting Bash telemetry beacon on $FLEET_NODE $FLEET_SIGN (port $PORT)..."
fi
prev_work=0
prev_total=0
while true; do
# 1. CPU
cpu_pct=0.0
if [ -r /proc/stat ]; then
read -r _ u n s i io irq sirq stl _ < /proc/stat 2>/dev/null || true
work=$(( u + n + s + irq + sirq + stl ))
total=$(( work + i + io ))
if [ "$prev_total" -gt 0 ] && [ "$total" -gt "$prev_total" ]; then
d_work=$(( work - prev_work ))
d_total=$(( total - prev_total ))
if [ "$d_total" -gt 0 ]; then
cpu_pct=$(awk -v w="$d_work" -v t="$d_total" 'BEGIN { printf "%.1f", (w/t)*100 }')
fi
fi
prev_work=$work
prev_total=$total
fi
# 2. RAM
mem_used=0.0
mem_total=0.0
if [ -r /proc/meminfo ]; then
tot_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
avail_kb=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
if [ -z "$avail_kb" ]; then
free_kb=$(awk '/^MemFree:/ {print $2}' /proc/meminfo)
buf_kb=$(awk '/^Buffers:/ {print $2}' /proc/meminfo)
cache_kb=$(awk '/^Cached:/ {print $2}' /proc/meminfo)
avail_kb=$(( free_kb + buf_kb + cache_kb ))
fi
if [ -n "$tot_kb" ] && [ "$tot_kb" -gt 0 ]; then
mem_total=$(awk -v t="$tot_kb" 'BEGIN { printf "%.1f", t/1048576 }')
used_kb=$(( tot_kb - avail_kb ))
mem_used=$(awk -v u="$used_kb" 'BEGIN { printf "%.1f", u/1048576 }')
fi
fi
# 3. Disk
disk_mount="/"
if [ "$FLEET_NODE" = "boxC" ] && [ -d "/mnt/kof17" ]; then
disk_mount="/mnt/kof17"
elif [ "$FLEET_NODE" = "bazina" ] && [ -d "/mnt/ssd" ]; then
disk_mount="/mnt/ssd"
fi
df_out=$(df -B1G "$disk_mount" 2>/dev/null | tail -n 1) || df_out=""
disk_total=$(echo "$df_out" | awk '{print $2}')
disk_free=$(echo "$df_out" | awk '{print $4}')
disk_total="${disk_total:-0.0}"
disk_free="${disk_free:-0.0}"
# 4. GPU (sysfs)
gpu_str=""
for gpath in /sys/class/drm/card*/device/gpu_busy_percent; do
if [ -r "$gpath" ]; then
gpu_val=$(cat "$gpath" 2>/dev/null || echo "")
[ -n "$gpu_val" ] && gpu_str="Navi10 ${gpu_val}%" && break
fi
done
# 5. Temp
temp_c=0
for tpath in /sys/class/thermal/thermal_zone0/temp /sys/class/hwmon/hwmon0/temp1_input; do
if [ -r "$tpath" ]; then
traw=$(cat "$tpath" 2>/dev/null || echo 0)
[ "$traw" -gt 0 ] && temp_c=$(( traw / 1000 )) && break
fi
done
# 6. Battery
bat_pct=-1
for bpath in /sys/class/power_supply/BAT*/capacity /sys/class/power_supply/*battery*/capacity; do
if [ -r "$bpath" ]; then
bval=$(cat "$bpath" 2>/dev/null || echo -1)
[ "$bval" -ge 0 ] && bat_pct=$bval && break
fi
done
# 7. Alert
alert=""
if [ "$FLEET_NODE" = "boxC" ] && [ "$disk_mount" = "/mnt/kof17" ]; then
awk_crit=$(awk -v df="$disk_free" 'BEGIN { print (df < 3.0) ? 1 : 0 }')
[ "$awk_crit" -eq 1 ] && alert="kof17 CRITICAL: ${disk_free}G free"
elif [ "$FLEET_NODE" = "crostini" ]; then
awk_crit=$(awk -v df="$disk_free" 'BEGIN { print (df < 1.0) ? 1 : 0 }')
[ "$awk_crit" -eq 1 ] && alert="crostini LOW DISK: ${disk_free}G free"
fi
if [ "$bat_pct" -ge 0 ] && [ "$bat_pct" -le 15 ]; then
alert="$FLEET_NODE BATTERY CRITICAL: ${bat_pct}%"
fi
ts=$(date +%s)
payload=$(printf '{"name":"%s","hostname":"%s","node":"%s","arch":"%s","sign":"%s","timestamp":%s,"cpu_percent":%s,"mem_used_gb":%s,"mem_total_gb":%s,"disk_mount":"%s","disk_free_gb":%s,"disk_total_gb":%s,"gpu_str":"%s","temp_c":%d,"battery_percent":%d,"alert":"%s"}' \
"$FLEET_NODE" "$RAW_HOST" "$FLEET_NODE" "$FLEET_ARCH" "$FLEET_SIGN" \
"$ts" "$cpu_pct" "$mem_used" "$mem_total" \
"$disk_mount" "$disk_free" "$disk_total" \
"$gpu_str" "$temp_c" "$bat_pct" "$alert")
# Broadcast UDP
for target in "${TARGETS[@]}"; do
if [ -w /dev/udp ]; then
echo -n "$payload" > "/dev/udp/$target/$PORT" 2>/dev/null || true
elif command -v socat >/dev/null 2>&1; then
echo -n "$payload" | socat - "UDP-DATAGRAM:$target:$PORT,broadcast" 2>/dev/null || true
elif command -v nc >/dev/null 2>&1; then
echo -n "$payload" | nc -u -w1 "$target" "$PORT" 2>/dev/null || true
fi
done
# Update SHM cache
shm_fleet="/dev/shm/fleet_telemetry.json"
tmp_path="${shm_fleet}.tmp.$$"
if [ -f "$shm_fleet" ]; then
# Merge using awk/sed
grep -v "\"${FLEET_NODE}\":" "$shm_fleet" 2>/dev/null | sed -e 's/^}$//' > "$tmp_path" || true
echo " ,\"${FLEET_NODE}\": $payload" >> "$tmp_path"
echo "}" >> "$tmp_path"
else
printf '{\n "%s": %s\n}\n' "$FLEET_NODE" "$payload" > "$tmp_path"
fi
chmod 666 "$tmp_path" 2>/dev/null || true
mv -f "$tmp_path" "$shm_fleet" 2>/dev/null || true
# WAN HTTP fallback
if [ "$FLEET_NODE" != "boxC" ]; then
if command -v curl >/dev/null 2>&1; then
curl -s -m 1.5 -X POST "http://91.139.118.43/telemetry.php" \
-H "Content-Type: application/json" -d "$payload" >/dev/null 2>&1 || true
fi
fi
if [ "$PRINT_JSON" -eq 1 ]; then
echo "$payload"
fi
[ "$RUN_ONCE" -eq 1 ] && break
sleep "$INTERVAL"
done
+502
View File
@@ -0,0 +1,502 @@
/* =============================================================================
* fleet-beacon.c — Multi-node Realtime Telemetry Beacon (Pure C POSIX)
* Observatoř Telemetry Platform // Fleet Operations
*
* Broadcasts UDP telemetry packets (port 9850) every 1.5s.
* Zero runtime dependencies, minimal RAM (< 1MB), 0.00% idle CPU.
* =============================================================================
*/
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/statvfs.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <time.h>
#include <glob.h>
#include <fcntl.h>
#include <errno.h>
#define BEACON_PORT 9850
#define INTERVAL_US 1500000
static const char *TARGETS[] = {
"255.255.255.255",
"192.168.1.255",
"192.168.1.100", /* t520 ETH */
"192.168.1.102", /* t520 WiFi */
"192.168.1.104", /* bazina */
"192.168.1.101", /* boxC */
"192.168.1.105", /* crostini */
"192.168.1.103", /* android */
"10.39.9.255", /* WireGuard Broadcast */
"10.39.9.1", /* boxC VPN */
"10.0.0.255",
"10.0.0.18",
"10.0.0.14",
"91.139.118.43",
"192.168.43.255",
"192.168.43.1",
"127.0.0.1",
NULL
};
static void resolve_identity(char *node, size_t n_len, char *arch, size_t a_len, char *sign, size_t s_len, char *raw_host, size_t h_len) {
/* 1. Hostname */
char hbuf[128] = {0};
if (gethostname(hbuf, sizeof(hbuf) - 1) != 0) {
snprintf(hbuf, sizeof(hbuf), "unknown");
}
char *dot = strchr(hbuf, '.');
if (dot) *dot = '\0';
snprintf(raw_host, h_len, "%s", hbuf);
/* 2. FLEET_NODE override / file probe */
const char *env_node = getenv("FLEET_NODE");
if (env_node && *env_node) {
snprintf(node, n_len, "%s", env_node);
} else {
char line[256];
FILE *fp = fopen("/etc/fleet_id", "r");
if (!fp) {
char home_f[256];
const char *home = getenv("HOME");
if (home) {
snprintf(home_f, sizeof(home_f), "%s/.fleet_id", home);
fp = fopen(home_f, "r");
}
}
if (fp) {
while (fgets(line, sizeof(line), fp)) {
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\n' || *p == '\r' || *p == '\0') continue;
if (strncmp(p, "FLEET_NODE=", 11) == 0) {
char *val = p + 11;
if (*val == '"' || *val == '\'') val++;
size_t l = strlen(val);
while (l > 0 && (val[l-1] == '\n' || val[l-1] == '\r' || val[l-1] == '"' || val[l-1] == '\'' || val[l-1] == ' ')) val[--l] = '\0';
snprintf(node, n_len, "%s", val);
} else if (strncmp(p, "FLEET_ARCH=", 11) == 0) {
char *val = p + 11;
if (*val == '"' || *val == '\'') val++;
size_t l = strlen(val);
while (l > 0 && (val[l-1] == '\n' || val[l-1] == '\r' || val[l-1] == '"' || val[l-1] == '\'' || val[l-1] == ' ')) val[--l] = '\0';
snprintf(arch, a_len, "%s", val);
} else if (strncmp(p, "FLEET_SIGN=", 11) == 0) {
char *val = p + 11;
if (*val == '"' || *val == '\'') val++;
size_t l = strlen(val);
while (l > 0 && (val[l-1] == '\n' || val[l-1] == '\r' || val[l-1] == '"' || val[l-1] == '\'' || val[l-1] == ' ')) val[--l] = '\0';
snprintf(sign, s_len, "%s", val);
} else if (node[0] == '\0') {
size_t l = strlen(p);
while (l > 0 && (p[l-1] == '\n' || p[l-1] == '\r' || p[l-1] == ' ')) p[--l] = '\0';
snprintf(node, n_len, "%s", p);
}
}
fclose(fp);
}
if (node[0] == '\0') {
if (access("/data/data/com.termux", F_OK) == 0 || getenv("TERMUX_VERSION") != NULL) {
snprintf(node, n_len, "android");
} else if (strcmp(hbuf, "localhost") == 0 || strcmp(hbuf, "android") == 0) {
snprintf(node, n_len, "android");
} else if (strcmp(hbuf, "penguin") == 0) {
snprintf(node, n_len, "crostini");
} else {
snprintf(node, n_len, "%s", hbuf);
}
}
}
/* 3. Arch */
const char *env_arch = getenv("FLEET_ARCH");
if (env_arch && *env_arch) {
snprintf(arch, a_len, "%s", env_arch);
} else {
struct utsname uts;
if (uname(&uts) == 0) {
snprintf(arch, a_len, "%s", uts.machine);
} else {
snprintf(arch, a_len, "unknown");
}
}
/* 4. Sign */
const char *env_sign = getenv("FLEET_SIGN");
if (env_sign && *env_sign) {
snprintf(sign, s_len, "%s", env_sign);
} else {
snprintf(sign, s_len, "[%s:%s]", node, arch);
}
}
static double read_cpu(unsigned long long *prev_work, unsigned long long *prev_total) {
FILE *fp = fopen("/proc/stat", "r");
if (!fp) return 0.0;
char line[256];
if (!fgets(line, sizeof(line), fp)) {
fclose(fp);
return 0.0;
}
fclose(fp);
unsigned long long user = 0, nice = 0, system = 0, idle = 0, iowait = 0, irq = 0, softirq = 0, steal = 0;
if (sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
&user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal) < 4) {
return 0.0;
}
unsigned long long work = user + nice + system + irq + softirq + steal;
unsigned long long total = work + idle + iowait;
double pct = 0.0;
if (*prev_total > 0 && total > *prev_total) {
unsigned long long d_work = work - *prev_work;
unsigned long long d_total = total - *prev_total;
pct = ((double)d_work / (double)d_total) * 100.0;
if (pct < 0.0) pct = 0.0;
if (pct > 100.0) pct = 100.0;
}
*prev_work = work;
*prev_total = total;
return pct;
}
static void read_memory(double *used_gb, double *total_gb) {
*used_gb = 0.0;
*total_gb = 0.0;
FILE *fp = fopen("/proc/meminfo", "r");
if (!fp) return;
char line[128];
double total_kb = 0.0, avail_kb = 0.0, free_kb = 0.0, buffers_kb = 0.0, cached_kb = 0.0;
int have_avail = 0;
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, "MemTotal:", 9) == 0) {
sscanf(line + 9, "%lf", &total_kb);
} else if (strncmp(line, "MemAvailable:", 13) == 0) {
sscanf(line + 13, "%lf", &avail_kb);
have_avail = 1;
} else if (strncmp(line, "MemFree:", 8) == 0) {
sscanf(line + 8, "%lf", &free_kb);
} else if (strncmp(line, "Buffers:", 8) == 0) {
sscanf(line + 8, "%lf", &buffers_kb);
} else if (strncmp(line, "Cached:", 7) == 0) {
sscanf(line + 7, "%lf", &cached_kb);
}
}
fclose(fp);
if (!have_avail) {
avail_kb = free_kb + buffers_kb + cached_kb;
}
*total_gb = total_kb / (1024.0 * 1024.0);
*used_gb = (total_kb - avail_kb) / (1024.0 * 1024.0);
if (*used_gb < 0.0) *used_gb = 0.0;
}
static void read_disk(const char *node, char *mount_out, size_t m_len, double *free_gb, double *total_gb) {
const char *candidates[4] = { "/", NULL, NULL, NULL };
if (strcmp(node, "boxC") == 0) {
candidates[0] = "/mnt/kof17";
candidates[1] = "/";
} else if (strcmp(node, "bazina") == 0) {
candidates[0] = "/mnt/ssd";
candidates[1] = "/mnt/seagate";
candidates[2] = "/";
}
for (int i = 0; candidates[i] != NULL; ++i) {
struct statvfs st;
if (statvfs(candidates[i], &st) == 0 && st.f_blocks > 0) {
snprintf(mount_out, m_len, "%s", candidates[i]);
*free_gb = ((double)st.f_bavail * (double)st.f_frsize) / (1024.0 * 1024.0 * 1024.0);
*total_gb = ((double)st.f_blocks * (double)st.f_frsize) / (1024.0 * 1024.0 * 1024.0);
return;
}
}
snprintf(mount_out, m_len, "/");
*free_gb = 0.0;
*total_gb = 0.0;
}
static void read_gpu(char *gpu_out, size_t g_len) {
gpu_out[0] = '\0';
glob_t gl;
if (glob("/sys/class/drm/card*/device/gpu_busy_percent", 0, NULL, &gl) == 0) {
for (size_t i = 0; i < gl.gl_pathc; ++i) {
FILE *fp = fopen(gl.gl_pathv[i], "r");
if (fp) {
int busy = 0;
if (fscanf(fp, "%d", &busy) == 1) {
snprintf(gpu_out, g_len, "Navi10 %d%%", busy);
fclose(fp);
break;
}
fclose(fp);
}
}
globfree(&gl);
}
}
static int read_temp(void) {
const char *paths[] = {
"/sys/class/thermal/thermal_zone0/temp",
"/sys/class/hwmon/hwmon0/temp1_input",
"/sys/class/hwmon/hwmon1/temp1_input",
NULL
};
for (int i = 0; paths[i] != NULL; ++i) {
FILE *fp = fopen(paths[i], "r");
if (fp) {
long temp_raw = 0;
if (fscanf(fp, "%ld", &temp_raw) == 1) {
fclose(fp);
return (int)(temp_raw / 1000);
}
fclose(fp);
}
}
return 0;
}
static int read_battery(void) {
glob_t gl;
const char *patterns[] = {
"/sys/class/power_supply/BAT*/capacity",
"/sys/class/power_supply/*battery*/capacity",
"/sys/class/power_supply/sbs-*/capacity",
NULL
};
for (int p = 0; patterns[p] != NULL; ++p) {
if (glob(patterns[p], 0, NULL, &gl) == 0) {
for (size_t i = 0; i < gl.gl_pathc; ++i) {
FILE *fp = fopen(gl.gl_pathv[i], "r");
if (fp) {
int cap = -1;
if (fscanf(fp, "%d", &cap) == 1) {
fclose(fp);
globfree(&gl);
return cap;
}
fclose(fp);
}
}
globfree(&gl);
}
}
return -1;
}
static void update_shm(const char *node, const char *payload) {
const char *shm_path = "/dev/shm/fleet_telemetry.json";
char tmp_path[256];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.%d", shm_path, (int)getpid());
/* Read existing file up to 64KB */
char in_buf[65536] = {0};
FILE *fp = fopen(shm_path, "r");
if (fp) {
size_t n = fread(in_buf, 1, sizeof(in_buf) - 1, fp);
in_buf[n] = '\0';
fclose(fp);
}
FILE *out = fopen(tmp_path, "w");
if (!out) return;
/* Write simple JSON envelope */
if (in_buf[0] == '{') {
/* Filter out existing node key and append new */
fprintf(out, "{\n");
int wrote_any = 0;
/* Scan and copy other nodes */
const char *known[] = { "t520", "bazina", "boxC", "crostini", "penguin", "android", "phone", NULL };
for (int k = 0; known[k] != NULL; ++k) {
if (strcmp(known[k], node) == 0) continue;
char needle[64];
snprintf(needle, sizeof(needle), "\"%s\":", known[k]);
char *pos = strstr(in_buf, needle);
if (pos) {
/* Find opening brace */
char *brace_start = strchr(pos, '{');
if (brace_start) {
int depth = 1;
char *ptr = brace_start + 1;
while (*ptr && depth > 0) {
if (*ptr == '{') depth++;
else if (*ptr == '}') depth--;
ptr++;
}
if (depth == 0) {
if (wrote_any) fprintf(out, ",\n");
fprintf(out, " \"%s\": ", known[k]);
fwrite(brace_start, 1, (size_t)(ptr - brace_start), out);
wrote_any = 1;
}
}
}
}
if (wrote_any) fprintf(out, ",\n");
fprintf(out, " \"%s\": %s\n}\n", node, payload);
} else {
fprintf(out, "{\n \"%s\": %s\n}\n", node, payload);
}
fclose(out);
chmod(tmp_path, 0666);
rename(tmp_path, shm_path);
}
static void send_wan_http(const char *payload) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return;
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 500000;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in srv;
memset(&srv, 0, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(80);
inet_pton(AF_INET, "91.139.118.43", &srv.sin_addr);
if (connect(sock, (struct sockaddr *)&srv, sizeof(srv)) == 0) {
char req[4096];
int req_len = snprintf(req, sizeof(req),
"POST /telemetry.php HTTP/1.1\r\n"
"Host: 91.139.118.43\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n\r\n"
"%s", strlen(payload), payload);
if (req_len > 0) {
write(sock, req, (size_t)req_len);
}
}
close(sock);
}
int main(int argc, char **argv) {
int run_once = 0;
int print_json = 0;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--once") == 0 || strcmp(argv[i], "-1") == 0) run_once = 1;
else if (strcmp(argv[i], "--json") == 0 || strcmp(argv[i], "-j") == 0) { run_once = 1; print_json = 1; }
}
char node[64], arch[65], sign[160], raw_host[64];
resolve_identity(node, sizeof(node), arch, sizeof(arch), sign, sizeof(sign), raw_host, sizeof(raw_host));
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sock >= 0) {
int broadcast = 1;
setsockopt(udp_sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
}
if (!print_json) {
printf("[fleet-beacon-c] Starting native telemetry beacon on %s %s (port %d)...\n",
node, sign, BEACON_PORT);
fflush(stdout);
}
unsigned long long prev_work = 0, prev_total = 0;
while (1) {
double cpu_pct = read_cpu(&prev_work, &prev_total);
double mem_used = 0.0, mem_total = 0.0;
read_memory(&mem_used, &mem_total);
char mount[64] = "/";
double disk_free = 0.0, disk_total = 0.0;
read_disk(node, mount, sizeof(mount), &disk_free, &disk_total);
char gpu_str[64];
read_gpu(gpu_str, sizeof(gpu_str));
int temp_c = read_temp();
int bat_pct = read_battery();
char alert[256] = "";
if (strcmp(node, "boxC") == 0 && strcmp(mount, "/mnt/kof17") == 0 && disk_free < 3.0) {
snprintf(alert, sizeof(alert), "kof17 CRITICAL: %.1fG free", disk_free);
} else if ((strcmp(node, "crostini") == 0 || strcmp(node, "penguin") == 0) && disk_free < 1.0) {
snprintf(alert, sizeof(alert), "crostini LOW DISK: %.1fG free", disk_free);
} else if (bat_pct >= 0 && bat_pct <= 15) {
snprintf(alert, sizeof(alert), "%s BATTERY CRITICAL: %d%%", node, bat_pct);
}
char payload[2048];
snprintf(payload, sizeof(payload),
"{\"name\":\"%s\",\"hostname\":\"%s\",\"node\":\"%s\",\"arch\":\"%s\",\"sign\":\"%s\","
"\"timestamp\":%.3f,\"cpu_percent\":%.1f,\"mem_used_gb\":%.1f,\"mem_total_gb\":%.1f,"
"\"disk_mount\":\"%s\",\"disk_free_gb\":%.1f,\"disk_total_gb\":%.1f,"
"\"gpu_str\":\"%s\",\"temp_c\":%d,\"battery_percent\":%d,\"alert\":\"%s\"}",
node, raw_host, node, arch, sign,
(double)time(NULL), cpu_pct, mem_used, mem_total,
mount, disk_free, disk_total,
gpu_str, temp_c, bat_pct, alert);
/* 1. Broadcast UDP */
if (udp_sock >= 0) {
size_t payload_len = strlen(payload);
for (int i = 0; TARGETS[i] != NULL; ++i) {
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(BEACON_PORT);
if (inet_pton(AF_INET, TARGETS[i], &sin.sin_addr) > 0) {
sendto(udp_sock, payload, payload_len, 0, (struct sockaddr *)&sin, sizeof(sin));
}
}
}
/* 2. Update local SHM cache */
update_shm(node, payload);
/* 3. HTTP WAN Fallback */
if (strcmp(node, "boxC") != 0) {
send_wan_http(payload);
}
if (print_json) {
printf("%s\n", payload);
fflush(stdout);
}
if (run_once) {
break;
}
usleep(INTERVAL_US);
}
if (udp_sock >= 0) close(udp_sock);
return 0;
}