Files
yunke/yunke-server/scripts/healthcheck.py
T

114 lines
3.9 KiB
Python

#!/usr/bin/env python
"""服务健康检查 + 自动拉起(Windows 计划任务每 5 分钟跑一次)。
本会话内 nohup 起的服务多次无声死掉(终端清理连带杀死进程),此脚本:
1. 检查后端(:8000)、前端(:5173)、调度器(进程名匹配)是否活着
2. 死了的自动拉起(独立进程,不依赖任何终端会话)
3. 结果写 JSON 行到 healthcheck.log
注册计划任务(管理员 PowerShell 执行一次):
schtasks /Create /TN "YunkeHealthcheck" /TR "<python路径> 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()