"""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()