#!/usr/bin/env python3 """获取Minecraft服务器运行状态(JSON输出)""" import sys, os, subprocess, re sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from mc_config import RCON_HOST, RCON_PORT, RCON_PASSWORD, MC_SERVER_DIR from mc_rcon import RCONClient def get_mc_pid(): try: result = subprocess.run( ["pgrep", "-f", "java.*(minecraft|forge|neoforge)"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0 and result.stdout.strip(): return int(result.stdout.strip().split()[0]) except Exception: pass return None def get_memory(pid): if not pid: return None try: with open(f"/proc/{pid}/status", "r") as f: for line in f: if line.startswith("VmRSS:"): kb = int(line.split()[1]) return {"mb": round(kb/1024), "gb": round(kb/1024/1024, 1)} except Exception: pass return None def get_cpu(pid): if not pid: return None try: with open(f"/proc/{pid}/stat", "r") as f: stat = f.read().split() utime, stime = int(stat[13]), int(stat[14]) clk_tck = os.sysconf(os.sysconf_names['SC_CLK_TCK']) starttime = int(stat[21]) with open("/proc/uptime", "r") as f: uptime = float(f.read().split()[0]) proc_uptime = uptime - (starttime / clk_tck) if proc_uptime > 0: return round(((utime + stime) / clk_tck) / proc_uptime * 100, 1) except Exception: pass return None def get_uptime(pid): if not pid: return None try: with open(f"/proc/{pid}/stat", "r") as f: stat = f.read().split() clk_tck = os.sysconf(os.sysconf_names['SC_CLK_TCK']) starttime = int(stat[21]) with open("/proc/uptime", "r") as f: uptime = float(f.read().split()[0]) sec = uptime - (starttime / clk_tck) d, h, m = int(sec//86400), int((sec%86400)//3600), int((sec%3600)//60) parts = [] if d: parts.append(f"{d}天") if h: parts.append(f"{h}小时") parts.append(f"{m}分钟") return "".join(parts) except Exception: return None def get_tps(): try: with RCONClient(RCON_HOST, RCON_PORT, RCON_PASSWORD) as client: client.send_command("spark tps") import time time.sleep(3) log_path = os.path.join(MC_SERVER_DIR, "logs", "latest.log") if not os.path.exists(log_path): return None with open(log_path, "r", errors="ignore") as f: lines = f.readlines() for line in reversed(lines[-500:]): if "tps" not in line.lower(): continue m = re.search(r'\*?([\d.]+),\s*\*?([\d.]+),\s*\*?([\d.]+),\s*\*?([\d.]+),\s*\*?([\d.]+)', line) if m: return { "tps_1m": float(m.group(3)), "raw": f"5s:{m.group(1)} 10s:{m.group(2)} 1m:{m.group(3)} 5m:{m.group(4)} 15m:{m.group(5)}" } except Exception: pass return None def get_version(): try: for vendor, name in [("neoforged", "NeoForge"), ("minecraftforge", "Forge")]: vdir = os.path.join(MC_SERVER_DIR, "libraries", "net", vendor, name.lower()) if os.path.exists(vdir): vers = [d for d in os.listdir(vdir) if os.path.isdir(os.path.join(vdir, d))] if vers: return f"{name} {vers[0]}" except Exception: pass return "未知" def get_world_sizes(): sizes = {} for w in ["world", "world_nether", "world_the_end"]: p = os.path.join(MC_SERVER_DIR, w) if os.path.exists(p): try: r = subprocess.run(["du", "-sh", p], capture_output=True, text=True, timeout=10) if r.returncode == 0: sizes[w] = r.stdout.split()[0] except Exception: sizes[w] = "?" try: r = subprocess.run(["du", "-sh", MC_SERVER_DIR], capture_output=True, text=True, timeout=30) if r.returncode == 0: sizes["total"] = r.stdout.split()[0] except Exception: pass try: st = os.statvfs("/") total = st.f_frsize * st.f_blocks used = st.f_frsize * (st.f_blocks - st.f_bfree) sizes["disk_total_gb"] = round(total/1024**3, 1) sizes["disk_used_gb"] = round(used/1024**3, 1) sizes["disk_pct"] = round((used/total)*100, 1) except Exception: pass return sizes def get_system_load(): try: with open("/proc/loadavg", "r") as f: parts = f.read().split() cpu_count = os.cpu_count() or 1 return { "load_1": float(parts[0]), "load_5": float(parts[1]), "load_15": float(parts[2]), "cpu_count": cpu_count, "load_pct": round((float(parts[0])/cpu_count)*100, 1) } except Exception: return None def get_player_list(): try: with RCONClient(RCON_HOST, RCON_PORT, RCON_PASSWORD) as client: resp = client.send_command("list") body = resp.get("body", "") if resp else "" m = re.search(r'players online:\s*(.+)?$', body, re.IGNORECASE) if m and m.group(1): return [p.strip() for p in m.group(1).split(",") if p.strip()] except Exception: pass return [] def get_player_count(): try: with RCONClient(RCON_HOST, RCON_PORT, RCON_PASSWORD) as client: resp = client.send_command("list") body = resp.get("body", "") if resp else "" m = re.search(r'There are (\d+) of a max of (\d+) players', body) if m: return {"online": int(m.group(1)), "max": int(m.group(2))} except Exception: pass return {"online": 0, "max": 0} if __name__ == "__main__": import json pid = get_mc_pid() result = { "online": bool(pid), "pid": pid, "players": get_player_count(), "player_list": get_player_list(), "memory_mb": get_memory(pid), "cpu_percent": get_cpu(pid), "uptime": get_uptime(pid), "tps": get_tps(), "version": get_version(), "world_sizes": get_world_sizes(), "system_load": get_system_load(), } print(json.dumps(result, ensure_ascii=False, indent=2))