diff options
| author | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
|---|---|---|
| committer | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
| commit | 5a8d568931d9b23ce0df1265d05259a7012081c9 (patch) | |
| tree | 4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/server.py | |
init: mostly vibed
Diffstat (limited to 'alfred/server.py')
| -rw-r--r-- | alfred/server.py | 266 |
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") |
