#!/usr/bin/env python """服务健康检查 + 自动拉起(Windows 计划任务每 5 分钟跑一次)。 本会话内 nohup 起的服务多次无声死掉(终端清理连带杀死进程),此脚本: 1. 检查后端(:8000)、前端(:5173)、调度器(进程名匹配)是否活着 2. 死了的自动拉起(独立进程,不依赖任何终端会话) 3. 结果写 JSON 行到 healthcheck.log 注册计划任务(管理员 PowerShell 执行一次): schtasks /Create /TN "YunkeHealthcheck" /TR " d:\\refactor\\yunke\\yunke-server\\scripts\\healthcheck.py" /SC MINUTE /MO 5 /RU SYSTEM /F 或手动跑: .venv/Scripts/python.exe scripts/healthcheck.py """ from __future__ import annotations import json import logging import os import subprocess import time import urllib.request logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("healthcheck") SERVER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(SERVER_DIR) PY = os.path.join(SERVER_DIR, ".venv", "Scripts", "python.exe") LOG = os.path.join(SERVER_DIR, "healthcheck.log") def http_ok(url: str, timeout: int = 5) -> bool: try: with urllib.request.urlopen(url, timeout=timeout) as resp: return resp.status == 200 except Exception: return False def process_alive(pattern: str) -> bool: """wmic 查 commandline 含 pattern 的进程是否存在。""" try: out = subprocess.run( ["wmic", "process", "where", f"commandline like '%{pattern}%'", "get", "processid"], capture_output=True, text=True, timeout=15, ).stdout return any(line.strip().isdigit() for line in out.splitlines()) except Exception: return False def start_backend() -> None: log.info("拉起后端 :8000") subprocess.Popen( [PY, "-m", "uvicorn", "app.api.main:app", "--port", "8000"], cwd=SERVER_DIR, env={**os.environ, "DB_URL": "sqlite+aiosqlite:///./smoke.db", "PYTHONIOENCODING": "utf-8"}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, ) def start_frontend() -> None: log.info("拉起前端 :5173") subprocess.Popen( ["cmd", "/c", "npm run dev"], cwd=os.path.join(PROJECT_DIR, "yunkes"), env={**os.environ, "PYTHONIOENCODING": "utf-8"}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, ) def start_scheduler() -> None: log.info("拉起调度器") subprocess.Popen( [PY, "-m", "app.sync.cli", "scheduler"], cwd=SERVER_DIR, env={**os.environ, "DB_URL": "sqlite+aiosqlite:///./smoke.db", "PYTHONIOENCODING": "utf-8"}, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, ) def main() -> None: result: dict = {"ts": time.strftime("%Y-%m-%d %H:%M:%S")} result["backend"] = "up" if http_ok("http://127.0.0.1:8000/api/health") else "down" if result["backend"] == "down": start_backend() result["frontend"] = "up" if http_ok("http://localhost:5173/") else "down" if result["frontend"] == "down": start_frontend() result["scheduler"] = "up" if process_alive("app.sync.cli scheduler") else "down" if result["scheduler"] == "down": start_scheduler() result["level"] = "info" if all(v == "up" for v in (result["backend"], result["frontend"], result["scheduler"])) else "warning" line = json.dumps(result, ensure_ascii=False) log.info(line) with open(LOG, "a", encoding="utf-8") as f: f.write(line + "\n") if __name__ == "__main__": main()