Files
AgentsMeeting/gateway/scripts/health_service.py
T
hmo ac34556588 fix: RDP tunnel SSH auth under LocalSystem — port 5807 + key path search + detached process
- http_port 5802→5807: 5802 occupied by wechat-hermes-gateway xmpp_bot, dual-listener caused ~50% request 400s
- _rdp_enable(): DETACHED_PROCESS so tunnel survives bot restart/crash
- ExitOnForwardFailure=yes so dead forward exits immediately instead of hanging
- stderr→rdp_tunnel_ssh.log for diagnosability
- -i explicit key path with multi-home search (LocalSystem ~ = systemprofile, not C:\Users\hmo)
- env injection HOME/USERPROFILE for ssh subprocess
- IdentitiesOnly=yes so -i key is used exclusively
- health_service.py: /health port 5802→5807 (was causing 30s restart loop)
- easytier.json + rdp.json: all 5802→5807 references updated
- rdp.json: add xiaoguo/Mac as RDP participant + architecture participant
2026-07-15 13:30:34 +08:00

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 :5802/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 :5802/health, restarts bot on failure."""
_svc_name_ = "xxm-health"
_svc_display_name_ = "AgentsMeeting xxm Health Service"
_svc_description_ = "Monitors xxm bot via HTTP :5802/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)