diff --git a/app.py b/app.py index a3caa39..ce9e9b9 100644 --- a/app.py +++ b/app.py @@ -541,7 +541,8 @@ def api_hub_self_update(): from lib.update_lib import schedule_pull_and_restart try: - result = schedule_pull_and_restart(delay_seconds=2.0) + # 同步 pull + 延迟重启;timeout 留给 git pull + result = schedule_pull_and_restart(delay_seconds=1.2) except Exception as e: result = {"ok": False, "msg": f"触发更新异常: {e}", "deferred": False} return jsonify(result), (200 if result.get("ok") else 400) diff --git a/lib/update_lib.py b/lib/update_lib.py index 487b6cc..ba36e4a 100644 --- a/lib/update_lib.py +++ b/lib/update_lib.py @@ -1,8 +1,7 @@ -"""中控本机快速更新:git pull + pm2 restart.""" +"""中控本机快速更新:先同步 git pull,再异步 pm2 restart.""" from __future__ import annotations import os -import shlex import subprocess import sys import threading @@ -23,14 +22,14 @@ def _repo_root() -> Path: return _ROOT -def _run_git(args: list[str], *, cwd: Path) -> tuple[bool, str]: +def _run_git(args: list[str], *, cwd: Path, timeout: float = 15) -> tuple[bool, str]: try: proc = subprocess.run( ["git", *args], cwd=str(cwd), capture_output=True, text=True, - timeout=15, + timeout=timeout, ) except FileNotFoundError: return False, "未找到 git" @@ -84,53 +83,42 @@ def read_git_version() -> dict[str, Any]: return out -def _run_pull_and_restart(root: Path, script: Path) -> None: - log_path = _UPDATE_LOG - try: - log_path.parent.mkdir(parents=True, exist_ok=True) - except Exception: - pass - env = os.environ.copy() - env["REPO"] = str(root) - if not (env.get("PM2_APP_NAME") or "").strip(): - env["PM2_APP_NAME"] = "crypto_okx_hub" - try: - with open(log_path, "a", encoding="utf-8") as logf: - logf.write( - time.strftime("%Y-%m-%d %H:%M:%S") - + f" start pull_and_restart REPO={root}\n" - ) - logf.flush() +def _pm2_app_name() -> str: + return (os.getenv("PM2_APP_NAME") or "").strip() or "crypto_okx_hub" + + +def _schedule_pm2_restart(*, delay_seconds: float = 1.5) -> None: + app_name = _pm2_app_name() + delay = max(0.8, float(delay_seconds)) + + def _worker() -> None: + time.sleep(delay) + _append_log(f"pm2 restart {app_name} --update-env") + try: proc = subprocess.run( - ["bash", str(script)], - cwd=str(root), - env=env, - stdout=logf, - stderr=subprocess.STDOUT, + ["pm2", "restart", app_name, "--update-env"], + capture_output=True, text=True, - timeout=180, + timeout=120, ) - logf.write( - time.strftime("%Y-%m-%d %H:%M:%S") - + f" finish returncode={proc.returncode}\n" + _append_log( + f"pm2 restart returncode={proc.returncode} " + f"out={(proc.stdout or proc.stderr or '')[:300]}" ) - except Exception as e: - _append_log("pull_and_restart error: " + (str(e) or "unknown")) + except Exception as e: + _append_log("pm2 restart error: " + (str(e) or "unknown")) + + threading.Thread(target=_worker, name="hub-pm2-restart", daemon=True).start() -def schedule_pull_and_restart(*, delay_seconds: float = 2.0) -> dict[str, Any]: +def schedule_pull_and_restart(*, delay_seconds: float = 1.5) -> dict[str, Any]: + """同步 git pull,再异步 pm2 restart(便于前端立刻知道是否有新版本).""" if not sys.platform.startswith("linux"): msg = f"仅 Linux 服务器支持更新(当前 platform={sys.platform})" _append_log("reject: " + msg) return {"ok": False, "msg": msg, "deferred": False, "platform": sys.platform} root = _repo_root() - script = root / "deploy" / "pull_and_restart.sh" - if not script.is_file(): - msg = f"缺少更新脚本: {script}" - _append_log("reject: " + msg) - return {"ok": False, "msg": msg, "deferred": False} - if not (root / "ecosystem.config.cjs").is_file(): msg = f"缺少 ecosystem.config.cjs: {root}" _append_log("reject: " + msg) @@ -140,41 +128,62 @@ def schedule_pull_and_restart(*, delay_seconds: float = 2.0) -> dict[str, Any]: now = time.time() with _update_lock: if now < _update_pending_until: - msg = "已有更新任务进行中,请稍后再试" - return {"ok": False, "msg": msg, "deferred": False} - _update_pending_until = now + 180.0 + return {"ok": False, "msg": "已有更新任务进行中,请稍后再试", "deferred": False} + _update_pending_until = now + 120.0 - version = read_git_version() - delay = max(1.0, float(delay_seconds)) + before = read_git_version() + before_sha = str(before.get("sha") or "") + _append_log(f"git pull start before={before.get('short_sha') or ''} root={root}") + ok_pull, pull_out = _run_git(["pull", "--ff-only"], cwd=root, timeout=90) + if not ok_pull: + # 回退再试普通 pull + ok_pull, pull_out = _run_git(["pull"], cwd=root, timeout=90) + after = read_git_version() + after_sha = str(after.get("sha") or "") + changed = bool(before_sha and after_sha and before_sha != after_sha) + _append_log( + f"git pull done ok={ok_pull} changed={changed} " + f"after={after.get('short_sha') or ''} msg={pull_out[:200]}" + ) - def _worker() -> None: - try: - time.sleep(delay) - _run_pull_and_restart(root, script) - finally: - with _update_lock: - # 保留短暂冷却,避免连点 - pass - - try: - t = threading.Thread(target=_worker, name="hub-self-update", daemon=True) - t.start() - _append_log( - f"accepted before={version.get('short_sha') or ''} delay={delay}s root={root}" - ) + if not ok_pull and not after.get("ok"): + with _update_lock: + _update_pending_until = 0.0 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 "", - "repo": str(root), - "log": str(_UPDATE_LOG), + "ok": False, + "msg": pull_out or "git pull 失败", + "deferred": False, + "before_sha": before_sha, + "before_short_sha": before.get("short_sha") or "", } + + # pull 失败但 HEAD 仍可读:仍尝试重启(可能网络问题但本地可跑) + try: + _schedule_pm2_restart(delay_seconds=delay_seconds) except Exception as e: with _update_lock: _update_pending_until = 0.0 - msg = str(e) or "触发更新失败" - _append_log("reject: " + msg) - return {"ok": False, "msg": msg, "deferred": False} + return {"ok": False, "msg": str(e) or "触发重启失败", "deferred": False} + + if not ok_pull: + msg = "git pull 未成功,已触发重启: " + (pull_out or "") + elif changed: + msg = f"已拉取新版本 {(before.get('short_sha') or '?')} → {(after.get('short_sha') or '?')},即将重启" + else: + msg = "代码已是最新,即将重启进程" + + return { + "ok": True, + "msg": msg, + "deferred": True, + "changed": changed, + "pull_ok": ok_pull, + "pull_msg": pull_out[:500], + "before_sha": before_sha, + "before_short_sha": before.get("short_sha") or "", + "after_sha": after_sha, + "after_short_sha": after.get("short_sha") or "", + "branch": after.get("branch") or before.get("branch") or "", + "repo": str(root), + "log": str(_UPDATE_LOG), + } diff --git a/static/settings_update.js b/static/settings_update.js index 2e7aa28..ad968cd 100644 --- a/static/settings_update.js +++ b/static/settings_update.js @@ -64,22 +64,38 @@ el.className = "update-status" + (cls ? " " + cls : ""); } - async function waitRecover(versionUrl, beforeSha, statusEl, label) { + async function waitRecover(versionUrl, beforeSha, statusEl, label, opts) { + const options = opts || {}; + const expectSha = options.expectSha || ""; + const alreadyLatest = !!options.alreadyLatest; let sawDown = false; - const maxTries = 45; + let stableOnline = 0; + const maxTries = 30; for (let i = 0; i < maxTries; i++) { - await sleep(2000); + await sleep(1500); const res = await fetchJson(versionUrl); const v = res.data || {}; const online = res.httpOk && (v.ok !== false) && (v.online !== false); if (!online) { sawDown = true; - setStatus(statusEl, "已受理 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending"); + stableOnline = 0; + setStatus(statusEl, "重启中 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending"); continue; } const afterSha = v.sha || ""; const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : ""); const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : ""; + if (expectSha && afterSha && afterSha === expectSha) { + setStatus( + statusEl, + (label || "更新完成") + + (beforeSha && beforeSha !== afterSha + ? ": " + (beforeShort || "?") + " → " + (afterShort || "?") + : " " + (afterShort || "")), + "ok" + ); + return v; + } if (afterSha && beforeSha && afterSha !== beforeSha) { setStatus(statusEl, (label || "更新完成") + ": " + (beforeShort || "?") + " → " + (afterShort || "?"), "ok"); return v; @@ -94,9 +110,21 @@ ); return v; } - setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending"); + stableOnline += 1; + // 重启很快时可能看不到掉线:连续在线数次且版本符合预期/未变 → 视为完成 + if (stableOnline >= 4) { + if (alreadyLatest || (afterSha && beforeSha && afterSha === beforeSha)) { + setStatus(statusEl, "完成(已是最新 " + (afterShort || "") + ",进程已重启)", "ok"); + return v; + } + if (expectSha && afterSha === expectSha) { + setStatus(statusEl, (label || "更新完成") + " " + (afterShort || ""), "ok"); + return v; + } + } + setStatus(statusEl, "等待重启生效… (" + (i + 1) + "/" + maxTries + ")", "pending"); } - setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查看 logs/hub_update.log", "err"); + setStatus(statusEl, "超时未确认,请点「刷新版本」或查看 logs/hub_update.log", "err"); return null; } @@ -147,13 +175,17 @@ btn.disabled = false; return; } - setStatus(statusEl, "已受理 → 等待上线…", "pending"); + setStatus(statusEl, data.msg || "已拉取,等待重启…", "pending"); try { await waitRecover( "/api/settings/update/hub/version", beforeSha || data.before_sha || "", statusEl, - "中控更新完成" + "中控更新完成", + { + expectSha: data.after_sha || "", + alreadyLatest: !data.changed, + } ); await refreshHubVersion(); } finally { diff --git a/templates/settings.html b/templates/settings.html index 255cf4f..d6269af 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -183,7 +183,7 @@ {% endblock %} {% block scripts %} - +