feat(beacon): native C beacon dispatcher with Bash fallback (zero Python dependency)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
fleet-beacon-c
|
||||
*.o
|
||||
*.a
|
||||
+31
-235
@@ -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
|
||||
|
||||
Executable
+242
@@ -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()
|
||||
Executable
+205
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user