Files
kefu/wechat_rpa/startup_update.py
T
2026-09-21 10:34:06 +08:00

175 lines
6.7 KiB
Python

"""Check public releases before authentication, without blocking the Qt thread."""
import threading
from pathlib import Path
from PySide6.QtCore import QObject, QTimer, QUrl, Signal
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QMessageBox
import backend_client
import desktop_updater
from app_version import APP_VERSION
class LoginUpdateCheck(QObject):
checked = Signal(object)
downloaded = Signal(object)
progress = Signal(int, int)
def __init__(self, dialog):
super().__init__(dialog)
self.dialog = dialog
self.phase = "idle"
self.release = {}
self.resume = None
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self._timeout)
self.checked.connect(self._checked)
self.downloaded.connect(self._downloaded)
self.progress.connect(self._progress)
dialog.finished.connect(self._closed)
@staticmethod
def _emit(signal, value):
try:
signal.emit(value)
except RuntimeError:
pass # The login window was closed while the worker was running.
def start(self, resume):
if self.phase != "idle":
return
self.resume = resume
self.phase = "checking"
self.dialog._busy(True, f"正在检查新版本 · 当前 v{APP_VERSION}…")
self.timer.start(8000)
def worker():
try:
settings = backend_client.load_settings()
result = {"release": desktop_updater.fetch_manifest(
settings["server_url"], backend_client.device_id(), timeout=6.0
)}
except Exception as exc:
result = {"error": str(exc)}
self._emit(self.checked, result)
threading.Thread(target=worker, daemon=True, name="prelogin-update-check").start()
def _closed(self, *_):
self.phase = "closed"
self.timer.stop()
def _timeout(self):
self._checked({"error": "检查更新超时"})
def _continue(self):
if self.phase in {"closed", "done"}:
return
self.phase = "done"
self.timer.stop()
self.resume()
def _checked(self, result):
if self.phase != "checking":
return
self.timer.stop()
status = result.get("release") or {}
if result.get("error"):
# An ordinary network failure must not prevent login. Preserve a
# previously known mandatory upgrade instead of silently bypassing it.
try:
cached = backend_client.cached_release_status()
status = cached if cached.get("force_upgrade") else {}
except Exception:
status = {}
else:
try:
settings = backend_client.load_settings()
settings["last_release"] = status if status.get("eligible", True) else {}
backend_client.save_settings(settings)
except Exception:
pass
if not status.get("update_available") or not status.get("eligible", True):
self._continue()
return
self.release = status
self.phase = "offering"
if status.get("auto_install") and status.get("ready_for_auto_update"):
self._download()
return
self._offer()
def _offer(self):
status = self.release
forced = bool(status.get("force_upgrade"))
prompt = QMessageBox(self.dialog)
prompt.setWindowTitle("需要更新后才能登录" if forced else "发现新版本")
prompt.setIcon(QMessageBox.Icon.Information)
prompt.setText(f"当前版本 v{APP_VERSION} → 新版本 v{status['latest_version']}")
prompt.setInformativeText("升级完成后将自动重新打开软件。" if forced else "是否现在更新?也可以稍后更新,继续登录。")
if status.get("release_notes"):
prompt.setDetailedText(str(status["release_notes"])[:4000])
upgrade = prompt.addButton("立即更新", QMessageBox.ButtonRole.AcceptRole)
prompt.addButton("退出软件" if forced else "稍后更新", QMessageBox.ButtonRole.RejectRole)
prompt.setDefaultButton(upgrade)
prompt.exec()
if self.phase == "closed":
return
if prompt.clickedButton() is not upgrade:
self.dialog.reject() if forced else self._continue()
elif status.get("ready_for_auto_update"):
self._download()
else:
# Older release entries may have only a manual download URL.
try:
url = desktop_updater._validated_download_url(status.get("download_url"))
if not QDesktopServices.openUrl(QUrl(url)):
raise desktop_updater.UpdateError("无法打开下载地址")
except Exception as exc:
QMessageBox.warning(self.dialog, "无法下载更新", str(exc))
self.dialog.reject() if forced else self._continue()
def _download(self):
self.phase = "downloading"
self.dialog._busy(True, f"正在下载 v{self.release['latest_version']},请稍候…")
def report(downloaded, total):
try:
self.progress.emit(downloaded, total)
except RuntimeError:
pass
def worker():
try:
installer = desktop_updater.download_update(self.release, report)
result = {"installer": str(installer)}
except Exception as exc:
result = {"error": str(exc)}
self._emit(self.downloaded, result)
threading.Thread(target=worker, daemon=True, name="prelogin-update-download").start()
def _progress(self, downloaded, total):
if self.phase != "downloading":
return
detail = f"{min(100, int(downloaded * 100 / total))}%" if total else f"{downloaded // 1024 // 1024} MB"
self.dialog._set_status(f"正在下载更新 · {detail}", "busy")
def _downloaded(self, result):
if self.phase != "downloading":
return
try:
if result.get("error"):
raise desktop_updater.UpdateError(result["error"])
desktop_updater.launch_installer(Path(result["installer"]), self.release["latest_version"])
except Exception as exc:
QMessageBox.warning(self.dialog, "更新暂未完成", str(exc))
self.dialog.reject() if self.release.get("force_upgrade") else self._continue()
return
# Login has not happened; returning from its dialog exits the process.
# The existing independent updater waits for exit, installs and relaunches.
self.dialog.reject()