Commit 9beb537fb19e0bb59cc1471ec7a399dd925f3540
Commits[COMMIT BEGIN]commit 9beb537fb19e0bb59cc1471ec7a399dd925f3540 Author: 0x4248 <[email protected]> Date: Tue Feb 3 11:41:19 2026 +0000 PulseWatch: init diff --git a/basic_install.sh b/basic_install.sh new file mode 100644 index 0000000..d656ec0 --- /dev/null +++ b/basic_install.sh @@ -0,0 +1,2 @@ +git clone https://github.com/0x4248/nexus /opt/nexus + diff --git a/toolkits/ESCPOS/src/server.py b/toolkits/ESCPOS/src/server.py index 0588690..e8585fa 100644 --- a/toolkits/ESCPOS/src/server.py +++ b/toolkits/ESCPOS/src/server.py @@ -151,4 +151,4 @@ def index(): # --- main ------------------------------------------------------------------ if __name__ == "__main__": - app.run(host="0.0.0.0", port=80, debug=False, use_reloader=False) + app.run(host="0.0.0.0", port=8001, debug=False, use_reloader=False) diff --git a/usr/net/Pulse/CMakeLists.txt b/usr/net/PulseArchive/CMakeLists.txt similarity index 100% rename from usr/net/Pulse/CMakeLists.txt rename to usr/net/PulseArchive/CMakeLists.txt diff --git a/usr/net/Pulse/src/client.cpp b/usr/net/PulseArchive/src/client.cpp similarity index 100% rename from usr/net/Pulse/src/client.cpp rename to usr/net/PulseArchive/src/client.cpp diff --git a/usr/net/Pulse/src/global.h b/usr/net/PulseArchive/src/global.h similarity index 100% rename from usr/net/Pulse/src/global.h rename to usr/net/PulseArchive/src/global.h diff --git a/usr/net/Pulse/src/server.cpp b/usr/net/PulseArchive/src/server.cpp similarity index 100% rename from usr/net/Pulse/src/server.cpp rename to usr/net/PulseArchive/src/server.cpp diff --git a/usr/net/Pulse/src/time.cpp b/usr/net/PulseArchive/src/time.cpp similarity index 100% rename from usr/net/Pulse/src/time.cpp rename to usr/net/PulseArchive/src/time.cpp diff --git a/usr/net/Pulse/src/time.h b/usr/net/PulseArchive/src/time.h similarity index 100% rename from usr/net/Pulse/src/time.h rename to usr/net/PulseArchive/src/time.h diff --git a/usr/net/PulseWatch/client.py b/usr/net/PulseWatch/client.py new file mode 100644 index 0000000..ec2a7d3 --- /dev/null +++ b/usr/net/PulseWatch/client.py @@ -0,0 +1,77 @@ +import time +import requests +import collector +import sys +import uuid +import socket + +SERVER = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:9000" +HOSTNAME = socket.gethostname() + +def send_stats(): + requests.post( + f"{SERVER}/ingest", + json=collector.collect(), + timeout=2 + ) + +def ping(): + ping_id = str(uuid.uuid4()) + t0 = time.time_ns() + + r = requests.get( + f"{SERVER}/ping", + params={"ts": t0, "id": ping_id}, + timeout=2 + ) + + t1 = time.time_ns() + r.raise_for_status() + + rtt_ms = (t1 - t0) / 1_000_000 + + # agent-side receipt + requests.post( + f"{SERVER}/ingest", + json={ + "hostname": HOSTNAME, + "event": "ping_received", + "ping_id": ping_id, + }, + timeout=2 + ) + + # controller-side RTT + requests.post( + f"{SERVER}/ingest", + json={ + "hostname": HOSTNAME, + "event": "ping_rtt", + "ping_id": ping_id, + "rtt_ms": round(rtt_ms, 3), + }, + timeout=2 + ) + +if __name__ == "__main__": + while True: + try: + send_stats() + ping_id, rtt = ping() + + # store RTT on controller side + requests.post( + f"{SERVER}/ingest", + json={ + "hostname": HOSTNAME, + "event": "ping_rtt", + "ping_id": ping_id, + "rtt_ms": round(rtt, 3), + }, + timeout=2 + ) + + except Exception: + pass + + time.sleep(5) diff --git a/usr/net/PulseWatch/collector.py b/usr/net/PulseWatch/collector.py new file mode 100644 index 0000000..c3d4809 --- /dev/null +++ b/usr/net/PulseWatch/collector.py @@ -0,0 +1,67 @@ +import socket +import time + +def _read_kv(path): + out = {} + with open(path) as f: + for line in f: + k, *v = line.split() + out[k.rstrip(":")] = int(v[0]) + return out + +def cpu_usage(): + with open("/proc/stat") as f: + cpu = list(map(int, f.readline().split()[1:])) + + idle = cpu[3] + cpu[4] + total = sum(cpu) + return idle, total + +_prev_idle, _prev_total = cpu_usage() + +def cpu_percent(): + global _prev_idle, _prev_total + idle, total = cpu_usage() + + didle = idle - _prev_idle + dtotal = total - _prev_total + + _prev_idle, _prev_total = idle, total + return round(100 * (1 - didle / dtotal), 2) + +def mem_info(): + m = _read_kv("/proc/meminfo") + used = m["MemTotal"] - m["MemAvailable"] + return { + "total_kb": m["MemTotal"], + "used_kb": used, + "free_kb": m["MemAvailable"], + } + +def net_bytes(): + rx = tx = 0 + with open("/proc/net/dev") as f: + for line in f.readlines()[2:]: + data = line.split() + rx += int(data[1]) + tx += int(data[9]) + return rx, tx + +def uptime(): + with open("/proc/uptime") as f: + return int(float(f.readline().split()[0])) + +def collect(): + rx, tx = net_bytes() + return { + "hostname": socket.gethostname(), + "timestamp": int(time.time()), + "status": "OK", + "faults": 0, + "cpu_percent": cpu_percent(), + "loadavg": open("/proc/loadavg").read().strip(), + "memory": mem_info(), + "uptime_sec": uptime(), + "net_rx_bytes": rx, + "net_tx_bytes": tx, + } diff --git a/usr/net/PulseWatch/install_client.sh b/usr/net/PulseWatch/install_client.sh new file mode 100644 index 0000000..1f2e702 --- /dev/null +++ b/usr/net/PulseWatch/install_client.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -eu + +INSTALL_DIR="/opt/nexus/usr/net/PulseWatch" +SERVICE="/etc/systemd/system/pulsewatch-client.service" + +SERVER_URL="${1:-http://localhost:9000}" + +echo "[+] Installing PulseWatch client" +echo " Server: ${SERVER_URL}" + +# permissions on code +chown -R root:root "$INSTALL_DIR" +chmod -R 755 "$INSTALL_DIR" + +# systemd service +cat > "$SERVICE" <<EOF +[Unit] +Description=PulseWatch Client +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/bin/python3 ${INSTALL_DIR}/client.py ${SERVER_URL} +WorkingDirectory=${INSTALL_DIR} +Restart=always +RestartSec=5 + +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable --now pulsewatch-client + +echo "[+] PulseWatch client installed and running" diff --git a/usr/net/PulseWatch/install_server.sh b/usr/net/PulseWatch/install_server.sh new file mode 100644 index 0000000..70be152 --- /dev/null +++ b/usr/net/PulseWatch/install_server.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +INSTALL_DIR="/opt/nexus/usr/net/PulseWatch" +DATA_DIR="/var/lib/pulsewatch" +SERVICE="/etc/systemd/system/pulsewatch-server.service" + +echo "[+] Installing PulseWatch server" + +# user +if ! id pulsewatch >/dev/null 2>&1; then + useradd -r -s /usr/sbin/nologin pulsewatch +fi + +# data dir +mkdir -p "$DATA_DIR" +chown pulsewatch:pulsewatch "$DATA_DIR" +chmod 750 "$DATA_DIR" + +# permissions on code +chown -R root:root "$INSTALL_DIR" +chmod -R 755 "$INSTALL_DIR" + +# systemd service +cat > "$SERVICE" <<EOF +[Unit] +Description=PulseWatch Server +After=network.target + +[Service] +ExecStart=/usr/bin/python3 ${INSTALL_DIR}/server.py +WorkingDirectory=${INSTALL_DIR} +User=pulsewatch +Group=pulsewatch +Restart=always +RestartSec=3 + +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=${DATA_DIR} + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable --now pulsewatch-server + +echo "[+] PulseWatch server installed and running" diff --git a/usr/net/PulseWatch/server.py b/usr/net/PulseWatch/server.py new file mode 100644 index 0000000..9bc8ec6 --- /dev/null +++ b/usr/net/PulseWatch/server.py @@ -0,0 +1,97 @@ +import json +import os +import time +import threading +import uuid +from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs + +DATA_DIR = "./data" +os.makedirs(DATA_DIR, exist_ok=True) + +lock = threading.Lock() + +def store_stats(host, payload): + path = os.path.join(DATA_DIR, f"{host}.json") + ts = str(int(time.time())) + + with lock: + data = {} + if os.path.exists(path): + with open(path, "r") as f: + data = json.load(f) + + data[ts] = payload + + with open(path, "w") as f: + json.dump(data, f) + +def store_ping(host, payload): + path = os.path.join(DATA_DIR, f"{host}_pings.json") + ts = str(int(time.time())) + + with lock: + data = {} + if os.path.exists(path): + with open(path, "r") as f: + data = json.load(f) + + data[ts] = payload + + with open(path, "w") as f: + json.dump(data, f) + + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + if self.path != "/ingest": + self.send_error(404) + return + + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + + try: + payload = json.loads(body) + except json.JSONDecodeError: + self.send_error(400) + return + + host = payload.get("hostname") or self.client_address[0] + + if payload.get("event") in ("ping_received", "ping_rtt"): + store_ping(host, payload) + else: + store_stats(host, payload) + + self.send_response(200) + self.end_headers() + self.wfile.write(b"OK") + + def do_GET(self): + if not self.path.startswith("/ping"): + self.send_error(404) + return + + qs = parse_qs(urlparse(self.path).query) + ts = qs.get("ts", [None])[0] + ping_id = qs.get("id", [None])[0] + + if ts is None or ping_id is None: + self.send_error(400) + return + + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({ + "ts": ts, + "ping_id": ping_id + }).encode()) + + def log_message(self, *_): + pass + +if __name__ == "__main__": + server = ThreadingHTTPServer(("0.0.0.0", 9000), Handler) + server.serve_forever()[COMMIT END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.