242 lines
8.3 KiB
Python
242 lines
8.3 KiB
Python
"""启动阶段的强制升级进度窗口。"""
|
|
|
|
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):
|
|
if self.result.automatic_update_error:
|
|
self.stage_label.setText(self.result.automatic_update_error)
|
|
self.action_button.setText("等待修复版本")
|
|
self.action_button.setEnabled(False)
|
|
return
|
|
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(),
|
|
staged.sha256,
|
|
)
|
|
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()
|