feat(update): add forced upgrade progress flow
This commit is contained in:
+7
-35
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
from .. import appconfig, chrome, diagnostics, update_check
|
||||
from ..version import APP_NAME, display_name
|
||||
@@ -38,6 +37,7 @@ if QT_IMPORT_ERROR is None:
|
||||
from .tabs.image_studio import ImageStudioPreviewDialog, ImageStudioTab
|
||||
from .tabs.settings import SettingsTab
|
||||
from .main_window import MainWindow
|
||||
from .update_dialog import ForcedUpdateDialog, UpdatePreparationWorker
|
||||
else:
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -74,42 +74,14 @@ def _write_update_check_diagnostic(message, *, result=None, exc=None):
|
||||
pass
|
||||
|
||||
|
||||
def _forced_update_details(result):
|
||||
online_version = result.latest_version or result.min_supported_version or "未知"
|
||||
lines = [
|
||||
f"当前版本:{result.current_version}",
|
||||
f"线上版本:{online_version}",
|
||||
]
|
||||
if result.min_supported_version:
|
||||
lines.append(f"最低支持版本:{result.min_supported_version}")
|
||||
if result.message:
|
||||
lines.append(f"升级说明:{result.message}")
|
||||
if result.download_url:
|
||||
lines.append("请下载新版,关闭程序后覆盖程序文件和 _internal/,保留 data/ 目录。")
|
||||
else:
|
||||
lines.append("版本接口未提供下载地址,请联系管理员获取新版后再使用。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _show_forced_update_dialog(result, *, parent=None, opener=None) -> bool:
|
||||
opener = opener or webbrowser.open
|
||||
box = QMessageBox(parent)
|
||||
box.setIcon(QMessageBox.Warning)
|
||||
box.setWindowTitle("必须升级")
|
||||
box.setText("当前版本已不能继续使用,请先升级到新版。")
|
||||
box.setInformativeText(_forced_update_details(result))
|
||||
download_button = box.addButton("下载新版", QMessageBox.AcceptRole)
|
||||
exit_button = box.addButton("退出程序", QMessageBox.RejectRole)
|
||||
if not result.download_url and hasattr(download_button, "setEnabled"):
|
||||
download_button.setEnabled(False)
|
||||
box.setDefaultButton(download_button if result.download_url else exit_button)
|
||||
box.exec()
|
||||
if box.clickedButton() is download_button and result.download_url:
|
||||
opener(result.download_url)
|
||||
def _show_forced_update_dialog(result, *, parent=None, dialog_factory=None) -> bool:
|
||||
dialog_class = dialog_factory or ForcedUpdateDialog
|
||||
dialog = dialog_class(result, parent=parent)
|
||||
dialog.exec()
|
||||
return False
|
||||
|
||||
|
||||
def _run_startup_update_gate(*, checker=None, opener=None) -> bool:
|
||||
def _run_startup_update_gate(*, checker=None, dialog_factory=None) -> bool:
|
||||
try:
|
||||
result = (checker or update_check.check_for_update)()
|
||||
except Exception as exc:
|
||||
@@ -119,7 +91,7 @@ def _run_startup_update_gate(*, checker=None, opener=None) -> bool:
|
||||
if result.error:
|
||||
_write_update_check_diagnostic("启动版本检查失败,已允许继续使用", result=result)
|
||||
if result.forced:
|
||||
return _show_forced_update_dialog(result, opener=opener)
|
||||
return _show_forced_update_dialog(result, dialog_factory=dialog_factory)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"""启动阶段的强制升级进度窗口。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QObject, QThread, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QProgressBar,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from .. import appconfig, diagnostics, update_installer, updater_entry
|
||||
|
||||
|
||||
def _format_bytes(value):
|
||||
value = max(0, int(value or 0))
|
||||
if value >= 1024 * 1024:
|
||||
return "%.1f MB" % (value / (1024 * 1024))
|
||||
if value >= 1024:
|
||||
return "%.1f KB" % (value / 1024)
|
||||
return "%d B" % value
|
||||
|
||||
|
||||
class UpdatePreparationWorker(QObject):
|
||||
stage_changed = Signal(str)
|
||||
progress_changed = Signal(int, int)
|
||||
succeeded = Signal(object)
|
||||
failed = Signal(str)
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, result, install_root, prepare=None):
|
||||
super().__init__()
|
||||
self.result = result
|
||||
self.install_root = install_root
|
||||
self.prepare = prepare or update_installer.prepare_update
|
||||
self._cancelled = False
|
||||
|
||||
@Slot()
|
||||
def run(self):
|
||||
try:
|
||||
metadata = update_installer.metadata_from_update_info(self.result)
|
||||
staged = self.prepare(
|
||||
metadata,
|
||||
self.install_root,
|
||||
cancelled=lambda: self._cancelled,
|
||||
progress=self.progress_changed.emit,
|
||||
stage_callback=self.stage_changed.emit,
|
||||
)
|
||||
if self._cancelled:
|
||||
raise update_installer.UpdateCancelled("已取消下载新版")
|
||||
self.succeeded.emit(staged)
|
||||
except update_installer.UpdateCancelled:
|
||||
self.failed.emit("已取消下载新版")
|
||||
except update_installer.UpdateInstallError as exc:
|
||||
self.failed.emit(str(exc))
|
||||
except Exception as exc:
|
||||
try:
|
||||
diagnostics.write_diagnostic_log(
|
||||
"准备自动更新失败",
|
||||
level="ERROR",
|
||||
step="startup_update_prepare",
|
||||
exc=exc,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self.failed.emit("准备新版失败,请稍后重试")
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
def cancel(self):
|
||||
self._cancelled = True
|
||||
|
||||
|
||||
class ForcedUpdateDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
result,
|
||||
*,
|
||||
parent=None,
|
||||
install_root=None,
|
||||
worker_factory=None,
|
||||
updater_launcher=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.result = result
|
||||
self.install_root = Path(install_root or appconfig.app_base_dir()).resolve()
|
||||
self.worker_factory = worker_factory or UpdatePreparationWorker
|
||||
self.updater_launcher = updater_launcher or updater_entry.copy_and_launch_updater
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
self.exit_requested = False
|
||||
self.update_started = False
|
||||
self._build_ui()
|
||||
self._validate_available_update()
|
||||
|
||||
def _build_ui(self):
|
||||
self.setWindowTitle("必须升级")
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(460)
|
||||
layout = QVBoxLayout(self)
|
||||
title = QLabel("当前版本已不能继续使用,请先升级到新版。")
|
||||
title.setWordWrap(True)
|
||||
layout.addWidget(title)
|
||||
online_version = self.result.latest_version or self.result.min_supported_version or "未知"
|
||||
details = "当前版本:%s\n线上版本:%s" % (
|
||||
self.result.current_version,
|
||||
online_version,
|
||||
)
|
||||
if self.result.message:
|
||||
details += "\n升级说明:%s" % self.result.message
|
||||
self.details_label = QLabel(details)
|
||||
self.details_label.setWordWrap(True)
|
||||
layout.addWidget(self.details_label)
|
||||
self.stage_label = QLabel("发现必须升级的新版本")
|
||||
layout.addWidget(self.stage_label)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
layout.addWidget(self.progress_bar)
|
||||
self.bytes_label = QLabel("等待开始")
|
||||
layout.addWidget(self.bytes_label)
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch(1)
|
||||
self.action_button = QPushButton("立即升级")
|
||||
self.exit_button = QPushButton("退出程序")
|
||||
self.action_button.clicked.connect(self.start_update)
|
||||
self.exit_button.clicked.connect(self.request_exit)
|
||||
buttons.addWidget(self.action_button)
|
||||
buttons.addWidget(self.exit_button)
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _validate_available_update(self):
|
||||
try:
|
||||
update_installer.metadata_from_update_info(self.result)
|
||||
except update_installer.UpdateInstallError as exc:
|
||||
self._show_failure(str(exc))
|
||||
|
||||
@Slot()
|
||||
def start_update(self):
|
||||
if self.thread is not None:
|
||||
return
|
||||
self.exit_requested = False
|
||||
self.action_button.setEnabled(False)
|
||||
self.exit_button.setText("取消并退出")
|
||||
self.stage_label.setText("正在准备新版")
|
||||
self.worker = self.worker_factory(self.result, self.install_root)
|
||||
self.thread = QThread(self)
|
||||
self.worker.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.worker.stage_changed.connect(self.stage_label.setText)
|
||||
self.worker.progress_changed.connect(self._update_progress)
|
||||
self.worker.succeeded.connect(self._on_prepared)
|
||||
self.worker.failed.connect(self._on_failed)
|
||||
self.worker.finished.connect(self.thread.quit)
|
||||
self.worker.finished.connect(self.worker.deleteLater)
|
||||
self.thread.finished.connect(self._on_thread_finished)
|
||||
self.thread.start()
|
||||
|
||||
@Slot(int, int)
|
||||
def _update_progress(self, downloaded, total):
|
||||
percent = int(downloaded * 100 / total) if total else 0
|
||||
self.progress_bar.setValue(max(0, min(100, percent)))
|
||||
self.bytes_label.setText("已下载 %s / %s" % (_format_bytes(downloaded), _format_bytes(total)))
|
||||
|
||||
@Slot(object)
|
||||
def _on_prepared(self, staged):
|
||||
self.stage_label.setText("新版准备完成,正在重启")
|
||||
self.progress_bar.setValue(100)
|
||||
try:
|
||||
plan_path = updater_entry.create_plan(
|
||||
self.install_root,
|
||||
staged.staging_dir,
|
||||
staged.version,
|
||||
os.getpid(),
|
||||
)
|
||||
updater_path = self.install_root / "cmshopee-updater.exe"
|
||||
self.updater_launcher(updater_path, plan_path)
|
||||
self.update_started = True
|
||||
except Exception as exc:
|
||||
try:
|
||||
diagnostics.write_diagnostic_log(
|
||||
"启动独立更新器失败",
|
||||
level="ERROR",
|
||||
step="startup_update_launch",
|
||||
exc=exc,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self._show_failure("启动更新程序失败,请重试或联系管理员")
|
||||
|
||||
@Slot(str)
|
||||
def _on_failed(self, message):
|
||||
if self.exit_requested:
|
||||
self.reject()
|
||||
return
|
||||
self._show_failure(message)
|
||||
|
||||
@Slot()
|
||||
def _on_thread_finished(self):
|
||||
thread = self.thread
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
if thread is not None:
|
||||
thread.deleteLater()
|
||||
if self.update_started:
|
||||
self.accept()
|
||||
|
||||
def _show_failure(self, message):
|
||||
self.stage_label.setText("升级失败:%s" % message)
|
||||
self.action_button.setText("重试")
|
||||
self.action_button.setEnabled(True)
|
||||
self.exit_button.setText("退出程序")
|
||||
|
||||
@Slot()
|
||||
def request_exit(self):
|
||||
if self.worker is not None:
|
||||
self.exit_requested = True
|
||||
self.action_button.setEnabled(False)
|
||||
self.exit_button.setEnabled(False)
|
||||
self.stage_label.setText("正在取消下载并清理临时文件")
|
||||
self.worker.cancel()
|
||||
return
|
||||
self.reject()
|
||||
|
||||
def reject(self):
|
||||
if self.worker is not None:
|
||||
self.request_exit()
|
||||
return
|
||||
super().reject()
|
||||
Reference in New Issue
Block a user