ef2df073a8
Co-Authored-By: Claude <noreply@anthropic.com>
114 lines
3.9 KiB
Python
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()
|