aboutsummaryrefslogtreecommitdiffstats
path: root/alfred/server.py
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/server.py
init: mostly vibed
Diffstat (limited to 'alfred/server.py')
-rw-r--r--alfred/server.py266
1 files changed, 266 insertions, 0 deletions
diff --git a/alfred/server.py b/alfred/server.py
new file mode 100644
index 0000000..cfb4c69
--- /dev/null
+++ b/alfred/server.py
@@ -0,0 +1,266 @@
+"""
+Sopel plugin: server stats
+
+Commands (owner only) - use !server or !srv:
+ !server - show this usage
+ !server uptime - uptime and load averages
+ !server disk - disk usage for /
+ !server mem - RAM and swap usage
+ !server load - 1/5/15 min load + process count
+ !server status [service] - rc-service status (all or one)
+ !server net [iface] - RX/TX for iface (default: first non-lo)
+ !server top [n] - top n procs by CPU (default 5)
+ !server topmem [n] - top n procs by memory (default 5)
+ !server io - disk read/write stats for main block device
+ !server conns - TCP connection summary
+ !server who - logged-in users
+ !server logs <service> [n] - last n lines of service log (default 10)
+"""
+
+import os
+import subprocess
+
+from sopel import plugin
+import os as _os, sys as _sys
+_alfred_dir = _os.path.dirname(_os.path.abspath(__file__))
+if _alfred_dir not in _sys.path:
+ _sys.path.insert(0, _alfred_dir)
+import helpstrings as h
+
+SERVICES = [
+ "nginx", "postfix", "dovecot", "ngircd", "soju",
+ "opendkim", "fcgiwrap", "alfred", "dcron", "0x0",
+]
+
+LOG_PATHS = {
+ "nginx": ["/var/log/nginx/error.log", "/var/log/nginx/access.log"],
+ "postfix": ["/var/log/mail.log", "/var/log/messages"],
+ "dovecot": ["/var/log/dovecot.log", "/var/log/messages"],
+ "ngircd": ["/var/log/ngircd.log", "/var/log/messages"],
+ "soju": ["/var/log/soju.log", "/var/log/messages"],
+ "alfred": ["/var/log/alfred/sopel.log", "/var/log/alfred/daemon.log"],
+ "herald": ["/var/log/herald/sopel.log", "/var/log/herald/daemon.log"],
+}
+
+
+def _is_owner(bot, trigger):
+ return trigger.nick == bot.settings.core.owner
+
+
+def _run(cmd, timeout=10):
+ try:
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+ out = (r.stdout + r.stderr).strip()
+ return out[:400] if out else "(no output)"
+ except subprocess.TimeoutExpired:
+ return "(timed out)"
+ except Exception as e:
+ return f"(error: {e})"
+
+
+def _uptime(bot, _args):
+ bot.say(_run(["uptime"]))
+
+
+def _disk(bot, _args):
+ bot.say(_run(["df", "-h", "/"]))
+
+
+def _mem(bot, _args):
+ bot.say(_run(["free", "-h"]))
+
+
+def _load(bot, _args):
+ try:
+ with open("/proc/loadavg") as f:
+ fields = f.read().split()
+ load = f"{fields[0]} {fields[1]} {fields[2]}"
+ procs = fields[3]
+ bot.say(f"load: {load} | procs: {procs}")
+ except OSError as e:
+ bot.say(f"error: {e}")
+
+
+def _status(bot, args):
+ targets = [args[0]] if args else SERVICES
+ results = []
+ for svc in targets:
+ r = subprocess.run(
+ ["rc-service", svc, "status"],
+ capture_output=True, text=True, timeout=5
+ )
+ out = (r.stdout + r.stderr).strip()
+ state = "started" if "started" in out else "stopped" if "stopped" in out else out[:20]
+ results.append(f"{svc}:{state}")
+ bot.say(" | ".join(results))
+
+
+def _net(bot, args):
+ iface = args[0] if args else None
+ try:
+ with open("/proc/net/dev") as f:
+ lines = f.readlines()[2:]
+ devs = {}
+ for line in lines:
+ parts = line.split()
+ name = parts[0].rstrip(":")
+ if name == "lo":
+ continue
+ devs[name] = {"rx": int(parts[1]), "tx": int(parts[9])}
+
+ if not devs:
+ bot.say("net: no interfaces found")
+ return
+
+ target = iface if iface in devs else next(iter(devs))
+ if iface and iface not in devs:
+ bot.say(f"net: unknown iface '{iface}', using {target}")
+
+ def fmt(n):
+ for unit in ("B", "KB", "MB", "GB"):
+ if n < 1024:
+ return f"{n:.1f}{unit}"
+ n /= 1024
+ return f"{n:.1f}TB"
+
+ d = devs[target]
+ bot.say(f"{target}: RX {fmt(d['rx'])} | TX {fmt(d['tx'])}")
+ except OSError as e:
+ bot.say(f"error: {e}")
+
+
+def _top_procs(bot, args, sort_col, label):
+ try:
+ n = int(args[0]) if args else 5
+ n = max(1, min(n, 15))
+ except ValueError:
+ bot.say("usage: !srv top [n]")
+ return
+
+ r = subprocess.run(["top", "-b", "-n", "1"], capture_output=True, text=True)
+ lines = r.stdout.strip().splitlines()
+
+ proc_lines = []
+ in_procs = False
+ for line in lines:
+ if not in_procs:
+ if "PID" in line and "PPID" in line:
+ in_procs = True
+ continue
+ if line.strip():
+ proc_lines.append(line)
+
+ if not proc_lines:
+ bot.say("no processes found")
+ return
+
+ # columns: PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
+ procs = []
+ for line in proc_lines:
+ parts = line.split(None, 8)
+ if len(parts) < 9:
+ continue
+ try:
+ val = float(parts[sort_col].rstrip("%m"))
+ user = parts[2]
+ cmd = parts[8].split()[0].split("/")[-1][:25]
+ procs.append((val, user, cmd))
+ except (ValueError, IndexError):
+ continue
+
+ procs.sort(reverse=True)
+ out = " | ".join(f"{cmd}({user},{v:.1f}%)" for v, user, cmd in procs[:n])
+ bot.say(f"{label}: {out}" if out else f"{label}: (no data)")
+
+
+def _top(bot, args):
+ _top_procs(bot, args, sort_col=7, label="cpu")
+
+
+def _topmem(bot, args):
+ _top_procs(bot, args, sort_col=5, label="mem")
+
+
+def _io(bot, _args):
+ try:
+ with open("/proc/diskstats") as f:
+ lines = f.readlines()
+ for line in lines:
+ parts = line.split()
+ if len(parts) < 14:
+ continue
+ name = parts[2]
+ if name.startswith(("sd", "vd", "nvme", "hd", "mmcblk")) and not name[-1].isdigit():
+ reads = int(parts[5])
+ writes = int(parts[9])
+ bot.say(f"{name}: reads {reads} | writes {writes}")
+ return
+ bot.say("io: no block device found")
+ except OSError as e:
+ bot.say(f"error: {e}")
+
+
+def _conns(bot, _args):
+ bot.say(_run(["ss", "-s"]))
+
+
+def _who(bot, _args):
+ out = _run(["who"])
+ bot.say(out if out.strip() else "who: no users logged in")
+
+
+def _logs(bot, args):
+ if not args:
+ bot.say("usage: !srv logs <service> [n]")
+ return
+ svc = args[0]
+ try:
+ n = int(args[1]) if len(args) > 1 else 10
+ n = max(1, min(n, 30))
+ except ValueError:
+ bot.say("usage: !srv logs <service> [n]")
+ return
+
+ candidates = LOG_PATHS.get(svc, [f"/var/log/{svc}.log", f"/var/log/{svc}/error.log"])
+ for path in candidates:
+ if os.path.exists(path):
+ out = _run(["tail", "-n", str(n), path])
+ for line in out.splitlines()[:n]:
+ bot.say(line)
+ return
+ bot.say(f"logs: no log file found for '{svc}'")
+
+
+_SUBCOMMANDS = {
+ "uptime": _uptime,
+ "disk": _disk,
+ "mem": _mem,
+ "load": _load,
+ "status": _status,
+ "net": _net,
+ "top": _top,
+ "topmem": _topmem,
+ "io": _io,
+ "conns": _conns,
+ "who": _who,
+ "logs": _logs,
+}
+
+
+@plugin.commands("server", "srv")
+def cmd_server(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+
+ args = (trigger.group(2) or "").split()
+ sub = args[0].lower() if args else ""
+
+ if not sub:
+ bot.say(h.SRV_TOPIC)
+ return
+
+ fn = _SUBCOMMANDS.get(sub)
+ if fn:
+ fn(bot, args[1:])
+ else:
+ bot.say(f"server: unknown subcommand '{sub}' - type !srv for usage")