feat(update): add forced upgrade progress flow
This commit is contained in:
@@ -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