188 lines
7.2 KiB
Python
188 lines
7.2 KiB
Python
"""
|
|
Windows Service — xxm Bot Health Monitor
|
|
=========================================
|
|
Replaces Task Scheduler-based health_check_xxm.py.
|
|
|
|
Design:
|
|
- HTTP health check (GET :5807/health), NO subprocess polling
|
|
- Zero console windows (WMIC/tasklist-free during normal operation)
|
|
- Manageable via services.msc or `net start/stop xxm-health`
|
|
|
|
Belongs to: projects/AgentsMeeting/gateway/scripts/
|
|
|
|
Usage:
|
|
python health_service.py install # register service (run as Admin)
|
|
python health_service.py start # start (or net start xxm-health)
|
|
python health_service.py stop # stop (or net stop xxm-health)
|
|
python health_service.py remove # unregister
|
|
|
|
After install: manage via services.msc → "AgentsMeeting xxm Health Service"
|
|
"""
|
|
import os, sys, time, json, urllib.request, logging
|
|
|
|
import win32serviceutil
|
|
import win32service
|
|
import win32event
|
|
import servicemanager
|
|
|
|
# ── Paths (absolute, service-safe) ──────────────────────────────────
|
|
_PROJECT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
_LOG_DIR = os.path.join(_PROJECT, "gateway", "logs")
|
|
_LOG_FILE = os.path.join(_LOG_DIR, "health_service.log")
|
|
_PYTHON = r"C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe"
|
|
_BOT_SCRIPT = os.path.join(_PROJECT, "xmpp_agent_core.py")
|
|
|
|
# Bridge API key — needed because /health endpoint requires auth
|
|
_BRIDGE_API_KEY = "xxm_bridge_8f3a2c"
|
|
|
|
_CHECK_INTERVAL = 30 # seconds between health checks
|
|
_HEALTH_TIMEOUT = 10 # HTTP request timeout
|
|
_RESTART_WAIT = 3 # seconds to wait after kill before start
|
|
_STARTUP_WAIT = 5 # seconds to wait after start before first check
|
|
|
|
|
|
class HealthService(win32serviceutil.ServiceFramework):
|
|
"""Windows Service: polls :5807/health, restarts bot on failure."""
|
|
|
|
_svc_name_ = "xxm-health"
|
|
_svc_display_name_ = "AgentsMeeting xxm Health Service"
|
|
_svc_description_ = "Monitors xxm bot via HTTP :5807/health, auto-restarts on failure. No console windows."
|
|
|
|
def __init__(self, args):
|
|
win32serviceutil.ServiceFramework.__init__(self, args)
|
|
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
|
|
self._bot_pid: int | None = None
|
|
self._setup_logging()
|
|
|
|
# ── Logging ─────────────────────────────────────────────────────
|
|
|
|
def _setup_logging(self):
|
|
os.makedirs(_LOG_DIR, exist_ok=True)
|
|
logging.basicConfig(
|
|
filename=_LOG_FILE, level=logging.INFO,
|
|
format="%(asctime)s [SVC] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
def _log(self, msg: str):
|
|
logging.info(msg)
|
|
# Also write to Windows Event Log
|
|
servicemanager.LogMsg(
|
|
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
|
servicemanager.PYS_SERVICE_STARTED,
|
|
(self._svc_name_, f"[SVC] {msg}"),
|
|
)
|
|
|
|
# ── Service lifecycle ───────────────────────────────────────────
|
|
|
|
def SvcStop(self):
|
|
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
|
win32event.SetEvent(self.hWaitStop)
|
|
self._log("Service stopping")
|
|
|
|
def SvcDoRun(self):
|
|
servicemanager.LogMsg(
|
|
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
|
servicemanager.PYS_SERVICE_STARTED,
|
|
(self._svc_name_, ""),
|
|
)
|
|
self._log(f"Service started (check every {_CHECK_INTERVAL}s)")
|
|
|
|
# Acquire singleton lock via proc_guard
|
|
sys.path.insert(0, os.path.join(_PROJECT, "gateway", "scripts"))
|
|
try:
|
|
from proc_guard import guard
|
|
lock = guard("health_service_xxm")
|
|
if not lock.ok:
|
|
self._log(f"Another instance running: {lock.message} — exiting")
|
|
return
|
|
except Exception as e:
|
|
self._log(f"proc_guard init skipped: {e}")
|
|
|
|
# Ensure bot is healthy on startup
|
|
if not self._health_check():
|
|
self._log("Bot not responding on startup — restarting")
|
|
self._restart_bot()
|
|
|
|
# Main loop: wait(interval) → health check → loop
|
|
while True:
|
|
rc = win32event.WaitForSingleObject(self.hWaitStop, _CHECK_INTERVAL * 1000)
|
|
if rc == win32event.WAIT_OBJECT_0:
|
|
break # Stop signal
|
|
|
|
if not self._health_check():
|
|
self._log("Health check FAILED — restarting bot")
|
|
self._restart_bot()
|
|
|
|
self._log("Service stopped")
|
|
|
|
# ── Health check (HTTP only, zero subprocess) ───────────────────
|
|
|
|
def _health_check(self) -> bool:
|
|
"""GET :5807/health. Returns True if XMPP connected.
|
|
|
|
Port is 5807 (not 5802) — 5802 is occupied by the wechat-hermes-gateway
|
|
xmpp_bot.py (independent service). xxm bot moved to 5807 to avoid
|
|
dual-listener conflict; this health service must follow.
|
|
"""
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"http://127.0.0.1:5807/health?key={_BRIDGE_API_KEY}",
|
|
headers={"User-Agent": "health-service/1.0"},
|
|
)
|
|
resp = json.loads(
|
|
urllib.request.urlopen(req, timeout=_HEALTH_TIMEOUT).read()
|
|
)
|
|
ok = resp.get("xmpp_connected", False)
|
|
if not ok:
|
|
self._log(f"/health returned xmpp_connected=false: {resp}")
|
|
return ok
|
|
except Exception as e:
|
|
self._log(f"/health unreachable: {e}")
|
|
return False
|
|
|
|
# ── Bot restart (kill by stored PID → Popen with CREATE_NO_WINDOW) ──
|
|
|
|
def _restart_bot(self):
|
|
import subprocess
|
|
|
|
# 1. Kill old bot by stored PID (no process enumeration needed)
|
|
if self._bot_pid:
|
|
try:
|
|
subprocess.run(
|
|
["taskkill", "/f", "/pid", str(self._bot_pid)],
|
|
capture_output=True, timeout=10,
|
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
)
|
|
self._log(f"Killed old bot (PID {self._bot_pid})")
|
|
except Exception as e:
|
|
self._log(f"Kill PID {self._bot_pid} failed: {e}")
|
|
self._bot_pid = None
|
|
|
|
time.sleep(_RESTART_WAIT)
|
|
|
|
# 2. Start new bot
|
|
try:
|
|
p = subprocess.Popen(
|
|
[_PYTHON, _BOT_SCRIPT, "--agent", "xxm"],
|
|
cwd=os.path.dirname(_BOT_SCRIPT),
|
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
self._bot_pid = p.pid
|
|
self._log(f"Bot started (PID {p.pid})")
|
|
|
|
# 3. Wait a moment then verify
|
|
time.sleep(_STARTUP_WAIT)
|
|
if self._health_check():
|
|
self._log("Bot healthy after restart")
|
|
else:
|
|
self._log("Bot started but /health not yet responding")
|
|
except Exception as e:
|
|
self._log(f"Bot start FAILED: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
win32serviceutil.HandleCommandLine(HealthService)
|