"""中控本机快速更新:git pull + pm2 restart.""" from __future__ import annotations import os import shlex import subprocess import sys import threading import time from pathlib import Path from typing import Any _ROOT = Path(__file__).resolve().parent.parent _update_lock = threading.Lock() _update_pending_until = 0.0 def _repo_root() -> Path: env_root = (os.getenv("REPO") or "").strip() if env_root: return Path(env_root) return _ROOT def _run_git(args: list[str], *, cwd: Path) -> tuple[bool, str]: try: proc = subprocess.run( ["git", *args], cwd=str(cwd), capture_output=True, text=True, timeout=15, ) except FileNotFoundError: return False, "未找到 git" except subprocess.TimeoutExpired: return False, "git 超时" except Exception as e: return False, str(e) or "git 失败" out = (proc.stdout or "").strip() or (proc.stderr or "").strip() return proc.returncode == 0, out def read_git_version() -> dict[str, Any]: root = _repo_root() out: dict[str, Any] = { "ok": True, "repo": str(root), "sha": "", "short_sha": "", "branch": "", "dirty": False, "msg": "", "online": True, } if not (root / ".git").exists() and not (root / ".git").is_file(): out["ok"] = False out["msg"] = "非 git 仓库" return out ok, sha = _run_git(["rev-parse", "HEAD"], cwd=root) if not ok or not sha: out["ok"] = False out["msg"] = sha or "无法读取 HEAD" return out out["sha"] = sha out["short_sha"] = sha[:7] ok_b, branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) if ok_b and branch: out["branch"] = branch ok_d, dirty = _run_git(["status", "--porcelain"], cwd=root) if ok_d: out["dirty"] = bool(dirty.strip()) return out def schedule_pull_and_restart(*, delay_seconds: float = 1.5) -> dict[str, Any]: if not sys.platform.startswith("linux"): return {"ok": False, "msg": "仅 Linux 服务器支持更新", "deferred": False} root = _repo_root() script = root / "deploy" / "pull_and_restart.sh" if not script.is_file(): return {"ok": False, "msg": f"缺少更新脚本: {script}", "deferred": False} global _update_pending_until now = time.time() with _update_lock: if now < _update_pending_until: return {"ok": False, "msg": "已有更新任务进行中,请稍后再试", "deferred": False} _update_pending_until = now + 120.0 version = read_git_version() try: delay = max(0.5, float(delay_seconds)) cmd = ( f"sleep {delay} && " f"cd {shlex.quote(str(root))} && " f"exec bash {shlex.quote(str(script))}" ) subprocess.Popen( ["bash", "-c", cmd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, cwd=str(root), ) return { "ok": True, "msg": "更新已受理,即将 git pull 并重启", "deferred": True, "before_sha": version.get("sha") or "", "before_short_sha": version.get("short_sha") or "", "branch": version.get("branch") or "", } except FileNotFoundError: with _update_lock: _update_pending_until = 0.0 return {"ok": False, "msg": "未找到 bash", "deferred": False} except Exception as e: with _update_lock: _update_pending_until = 0.0 return {"ok": False, "msg": str(e) or "触发更新失败", "deferred": False}