diff --git a/yunke-server/scripts/healthcheck.py b/yunke-server/scripts/healthcheck.py new file mode 100644 index 0000000..5f2a5a9 --- /dev/null +++ b/yunke-server/scripts/healthcheck.py @@ -0,0 +1,113 @@ +#!/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()