243 lines
7.0 KiB
Python
Executable File
243 lines
7.0 KiB
Python
Executable File
import psutil
|
|
import socket
|
|
import platform
|
|
import time
|
|
import os
|
|
import getpass
|
|
import json
|
|
import requests
|
|
import logging
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
)
|
|
|
|
|
|
def load_config():
|
|
cfg_path = Path(__file__).with_suffix("").parent / "config.json"
|
|
if cfg_path.exists():
|
|
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
return {
|
|
"server_url": "http://127.0.0.1",
|
|
"agent_token": "agent-secret-token",
|
|
"interval_minutes": 60,
|
|
"send_public_ip": False,
|
|
}
|
|
|
|
|
|
def get_local_ip():
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.settimeout(1)
|
|
s.connect(("8.8.8.8", 80))
|
|
ip = s.getsockname()[0]
|
|
s.close()
|
|
return ip
|
|
except Exception:
|
|
return "127.0.0.1"
|
|
|
|
|
|
def get_public_ip():
|
|
try:
|
|
r = requests.get("https://api.ipify.org", timeout=5)
|
|
return r.text.strip()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def get_uptime():
|
|
return int(time.time() - psutil.boot_time())
|
|
|
|
|
|
def get_disk_info():
|
|
disks = []
|
|
for part in psutil.disk_partitions(all=False):
|
|
try:
|
|
usage = psutil.disk_usage(part.mountpoint)
|
|
disks.append({
|
|
"device": part.device,
|
|
"mountpoint": part.mountpoint,
|
|
"total_gb": round(usage.total / (2**30), 1),
|
|
"used_gb": round(usage.used / (2**30), 1),
|
|
"free_gb": round(usage.free / (2**30), 1),
|
|
"percent": usage.percent,
|
|
})
|
|
except Exception:
|
|
continue
|
|
return disks
|
|
|
|
|
|
def get_active_sessions():
|
|
"""Return active user sessions similar to the `who` command.
|
|
|
|
Each session is a dict with keys: username, tty, login_time, origin.
|
|
Falls back to the current process user if `who` is unavailable.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["who", "-u"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return []
|
|
sessions = []
|
|
for line in result.stdout.strip().splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) < 5:
|
|
continue
|
|
username = parts[0]
|
|
tty = parts[1]
|
|
# login time is usually "YYYY-MM-DD HH:MM" (parts[2] and parts[3])
|
|
login_time = " ".join(parts[2:4]) if len(parts) >= 4 else ""
|
|
# The last field may be a comment in parentheses, e.g. (192.168.255.50)
|
|
origin = ""
|
|
if parts[-1].startswith("(") and parts[-1].endswith(")"):
|
|
origin = parts[-1][1:-1]
|
|
sessions.append({
|
|
"username": username,
|
|
"tty": tty,
|
|
"login_time": login_time,
|
|
"origin": origin,
|
|
})
|
|
return sessions
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def get_display_username(sessions):
|
|
"""Return a short username representation for the dashboard.
|
|
|
|
Single user -> username
|
|
Multiple -> comma-separated usernames
|
|
No sessions -> fallback to process user
|
|
"""
|
|
usernames = [s["username"] for s in sessions if s.get("username")]
|
|
if not usernames:
|
|
return getpass.getuser()
|
|
return ", ".join(dict.fromkeys(usernames))
|
|
|
|
|
|
def get_cpu_temp():
|
|
try:
|
|
temps = psutil.sensors_temperatures()
|
|
if temps:
|
|
for name, entries in temps.items():
|
|
for entry in entries:
|
|
if entry.current:
|
|
return round(entry.current, 1)
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_load_avg():
|
|
try:
|
|
return ", ".join(str(x) for x in os.getloadavg())
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def get_top_processes(limit=10, processes=None):
|
|
"""Return top processes by CPU usage.
|
|
|
|
If ``processes`` is provided, it should be a list of psutil.Process objects
|
|
whose CPU baseline has already been established (cpu_percent called once).
|
|
The actual CPU usage is measured with the next call to cpu_percent().
|
|
"""
|
|
if processes is None:
|
|
processes = []
|
|
for p in psutil.process_iter(["pid", "name"]):
|
|
try:
|
|
p.cpu_percent(interval=None)
|
|
processes.append(p)
|
|
except Exception:
|
|
continue
|
|
time.sleep(1)
|
|
|
|
procs = []
|
|
for p in processes:
|
|
try:
|
|
procs.append({
|
|
"pid": p.pid,
|
|
"name": p.name(),
|
|
"cpu_percent": round(p.cpu_percent(interval=None), 1),
|
|
"ram_percent": round(p.memory_percent(), 1),
|
|
})
|
|
except Exception:
|
|
continue
|
|
procs.sort(key=lambda x: x["cpu_percent"], reverse=True)
|
|
return procs[:limit]
|
|
|
|
|
|
def collect():
|
|
cfg = load_config()
|
|
sessions = get_active_sessions()
|
|
|
|
# Establish per-process CPU baseline first. The 1-second interval below for
|
|
# system CPU also gives us meaningful per-process CPU percentages without
|
|
# an extra delay.
|
|
baseline_procs = []
|
|
for p in psutil.process_iter(["pid", "name"]):
|
|
try:
|
|
p.cpu_percent(interval=None)
|
|
baseline_procs.append(p)
|
|
except Exception:
|
|
continue
|
|
|
|
vm = psutil.virtual_memory()
|
|
sm = psutil.swap_memory()
|
|
|
|
payload = {
|
|
"hostname": socket.gethostname(),
|
|
"username": get_display_username(sessions),
|
|
"sessions": sessions,
|
|
"os_info": f"{platform.system()} {platform.release()}",
|
|
"local_ip": get_local_ip(),
|
|
"public_ip": get_public_ip() if cfg.get("send_public_ip") else None,
|
|
"uptime_sec": get_uptime(),
|
|
"cpu_percent": psutil.cpu_percent(interval=1),
|
|
"ram_percent": vm.percent,
|
|
"ram_total_gb": round(vm.total / (2**30), 1),
|
|
"ram_used_gb": round(vm.used / (2**30), 1),
|
|
"ram_available_gb": round(vm.available / (2**30), 1),
|
|
"swap_total_gb": round(sm.total / (2**30), 1),
|
|
"swap_used_gb": round(sm.used / (2**30), 1),
|
|
"swap_free_gb": round(sm.free / (2**30), 1),
|
|
"swap_percent": sm.percent,
|
|
"disk_info": get_disk_info(),
|
|
"cpu_temp": get_cpu_temp(),
|
|
"load_avg": get_load_avg(),
|
|
"processes": get_top_processes(10, baseline_procs),
|
|
}
|
|
return payload, cfg
|
|
|
|
|
|
def send(payload, cfg):
|
|
url = cfg["server_url"].rstrip("/") + "/api/heartbeat"
|
|
headers = {"Authorization": f"Bearer {cfg['agent_token']}", "Content-Type": "application/json"}
|
|
try:
|
|
resp = requests.post(url, json=payload, headers=headers, timeout=15)
|
|
resp.raise_for_status()
|
|
logging.info("Heartbeat sent: %s", resp.status_code)
|
|
except Exception as e:
|
|
logging.error("Failed to send heartbeat: %s", e)
|
|
|
|
|
|
def main():
|
|
payload, cfg = collect()
|
|
send(payload, cfg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|