175 lines
6.7 KiB
Python
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()
|