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 %}
-
+