Files

105 lines
3.7 KiB
Python

"""Git 热更新:定期检查远端新版本,更新前先推送通知,再拉取、同步依赖并重启进程。
基于 GitPython。重启前会执行 `uv sync` 使依赖与新版本一致,失败则不重启。
"""
import asyncio
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path
import git
log = logging.getLogger("updater")
def _find_uv() -> str | None:
for name in ("uv", "uv.exe"):
path = shutil.which(name)
if path:
return path
# 常见安装位置兜底
for path in (
Path.home() / ".local" / "bin" / "uv",
Path.home() / ".cargo" / "bin" / "uv.exe",
):
if path.exists():
return str(path)
return None
class Updater:
def __init__(self, ob, cfg: dict):
self.ob = ob
self.interval = cfg.get("interval", 300)
self.groups = cfg.get("groups", []) or []
self.friends = cfg.get("friends", []) or []
self._updating = False
try:
self.repo = git.Repo(".")
except (git.InvalidGitRepositoryError, git.NoSuchPathError):
self.repo = None
def start(self):
if self.repo is None:
log.info("非 git 仓库,热更新已禁用")
return
self._task = asyncio.get_running_loop().create_task(self._loop())
async def stop(self):
if hasattr(self, "_task"):
self._task.cancel()
async def _loop(self):
log.info("热更新已启动: 每 %ds 检查一次远端", self.interval)
while True:
try:
await asyncio.sleep(self.interval)
if self.ob.connected and not self._updating:
await self.check_once()
except asyncio.CancelledError:
break
except Exception:
log.exception("热更新检查出错")
async def check_once(self):
"""检查远端;有更新则通知、拉取并重启进程。"""
self._updating = True
try:
await asyncio.to_thread(self.repo.remotes.origin.fetch)
refs = self.repo.remotes.origin.refs
remote = getattr(refs, "HEAD", None) or (refs[0] if refs else None)
if remote is None or remote.commit == self.repo.head.commit:
return
log.info("发现新版本: %s -> %s",
self.repo.head.commit.hexsha[:8], remote.commit.hexsha[:8])
if self.ob.connected and (self.groups or self.friends):
await self.ob.broadcast(
"[更新] 检测到新版本,正在更新并重启机器人...",
self.groups, self.friends)
await asyncio.to_thread(self.repo.remotes.origin.pull, "--ff-only")
# 同步依赖(pyproject/uv.lock 可能在新版本中有变化)
uv = _find_uv()
if uv is None:
log.warning("未找到 uv,跳过依赖同步,直接重启")
else:
log.info("执行 uv sync 同步依赖...")
r = await asyncio.to_thread(
subprocess.run,
[uv, "sync"], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=300,
)
if r.returncode != 0:
log.error("uv sync 失败,取消重启: %s", (r.stderr or "")[-500:])
return
log.info("依赖同步完成")
log.warning("更新完成,重启进程...")
await asyncio.sleep(1)
os.execv(sys.executable, [sys.executable] + sys.argv)
finally:
self._updating = False