From ef2df073a8d095672e0ae7d0803d8e05e53664e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 21 Aug 2026 15:10:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E6=9C=8D=E5=8A=A1=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E6=A3=80=E6=9F=A5=E8=84=9A=E6=9C=AC(=E6=A3=80?= =?UTF-8?q?=E6=B5=8B+=E6=8B=89=E8=B5=B7=E5=90=8E=E7=AB=AF/=E5=89=8D?= =?UTF-8?q?=E7=AB=AF/=E8=B0=83=E5=BA=A6=E5=99=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- yunke-server/scripts/healthcheck.py | 113 ++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 yunke-server/scripts/healthcheck.py 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()