改进:显示更新下载进度并就地保存
This commit is contained in:
+92
-22
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QWidget, QLabel, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
||||
QCheckBox, QMenu, QDialog,
|
||||
QCheckBox, QMenu, QDialog, QProgressDialog,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
||||
@@ -1652,6 +1652,7 @@ class MainWindow(QWidget):
|
||||
_op_done = pyqtSignal(str, bool)
|
||||
_update_done = pyqtSignal()
|
||||
_update_download_done = pyqtSignal(bool, str)
|
||||
_update_download_progress = pyqtSignal(int, int, int)
|
||||
|
||||
_status_ready = pyqtSignal(object)
|
||||
_node_switched = pyqtSignal() # 启动校验时代替用户切换了节点(刷新标题栏)
|
||||
@@ -1665,6 +1666,7 @@ class MainWindow(QWidget):
|
||||
self._start_in_progress = False
|
||||
self._proxy_state_lock = threading.RLock()
|
||||
self._status_busy = False # 防止状态轮询线程堆叠
|
||||
self._update_progress_dialog = None
|
||||
# 把全局 log() 函数接到 GUI 日志区
|
||||
global _log_func
|
||||
_log_func = self.log_signal.emit
|
||||
@@ -1672,6 +1674,7 @@ class MainWindow(QWidget):
|
||||
self._op_done.connect(self._after_op)
|
||||
self._update_done.connect(self._on_update_done)
|
||||
self._update_download_done.connect(self._on_update_download_done)
|
||||
self._update_download_progress.connect(self._on_update_download_progress)
|
||||
self._status_ready.connect(self._apply_status)
|
||||
self._node_switched.connect(self._on_node_switched)
|
||||
|
||||
@@ -2338,8 +2341,35 @@ class MainWindow(QWidget):
|
||||
QMessageBox.Yes,
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
self._show_update_download_progress()
|
||||
threading.Thread(target=self._download_update, daemon=True).start()
|
||||
|
||||
def _show_update_download_progress(self):
|
||||
"""在 GUI 线程创建下载进度窗口;网络线程只通过 signal 回传状态。"""
|
||||
dialog = QProgressDialog("正在准备下载更新…", None, 0, 100, self)
|
||||
dialog.setWindowTitle("正在下载更新")
|
||||
dialog.setWindowModality(Qt.WindowModal)
|
||||
dialog.setMinimumDuration(0)
|
||||
dialog.setAutoClose(False)
|
||||
dialog.setAutoReset(False)
|
||||
dialog.setValue(0)
|
||||
dialog.show()
|
||||
self._update_progress_dialog = dialog
|
||||
|
||||
def _on_update_download_progress(self, percent, downloaded, total):
|
||||
"""把后台下载进度渲染到进度窗口。"""
|
||||
dialog = self._update_progress_dialog
|
||||
if dialog is None:
|
||||
return
|
||||
if total > 0:
|
||||
dialog.setRange(0, 100)
|
||||
dialog.setValue(max(0, min(100, percent)))
|
||||
dialog.setLabelText(
|
||||
f"正在下载更新… {percent}%({_fmt_bytes(downloaded)} / {_fmt_bytes(total)})")
|
||||
else:
|
||||
dialog.setRange(0, 0)
|
||||
dialog.setLabelText(f"正在下载更新… 已下载 {_fmt_bytes(downloaded)}")
|
||||
|
||||
def _download_update(self):
|
||||
"""后台下载新版本 EXE 到 EXE 所在目录。
|
||||
|
||||
@@ -2357,58 +2387,98 @@ class MainWindow(QWidget):
|
||||
|
||||
# 对 URL 进行安全编码,兼容空格或已编码空格
|
||||
url = urllib.parse.quote(url, safe=':/?#[]@!$&\'()*+,;=%')
|
||||
save_path = PERSIST_DIR / f"Royirin Rosmontis Private Proxy {self._latest_version}.exe"
|
||||
# 冻结程序必须放到当前 EXE 的目录,便于用户关闭旧版后就地替换;
|
||||
# 源码调试则仍写入源码目录,避免误把测试文件写到 Python 安装目录。
|
||||
if getattr(sys, 'frozen', False) or "__compiled__" in globals():
|
||||
download_dir = Path(sys.executable).resolve().parent
|
||||
else:
|
||||
download_dir = PERSIST_DIR
|
||||
save_path = download_dir / f"Royirin Rosmontis Private Proxy {self._latest_version}.exe"
|
||||
part_path = save_path.with_suffix(save_path.suffix + ".part")
|
||||
log(f"正在下载新版本: {url}")
|
||||
log(f"保存到: {save_path}")
|
||||
|
||||
def fetch(target):
|
||||
# 同更新检查:按「使用代理更新」设置选择隧道或直连
|
||||
if _update_via_proxy():
|
||||
use_proxy = _update_via_proxy()
|
||||
started_here = _ensure_forwarder() if use_proxy else False
|
||||
try:
|
||||
if use_proxy:
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler(
|
||||
{"http": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
|
||||
else:
|
||||
opener = _pac_no_proxy_opener()
|
||||
with opener.open(urllib.request.Request(target), timeout=120) as resp:
|
||||
return resp.read()
|
||||
|
||||
started_here = _ensure_forwarder() if _update_via_proxy() else False
|
||||
if part_path.exists():
|
||||
part_path.unlink()
|
||||
downloaded = 0
|
||||
hasher = hashlib.sha256()
|
||||
last_percent = -1
|
||||
with opener.open(urllib.request.Request(url), timeout=120) as resp, part_path.open("wb") as fp:
|
||||
try:
|
||||
data = fetch(url)
|
||||
response_size = int(resp.headers.get("Content-Length") or 0)
|
||||
except (TypeError, ValueError):
|
||||
response_size = 0
|
||||
total = self._asset_size or response_size
|
||||
self._update_download_progress.emit(0, 0, total)
|
||||
log(f"更新下载进度: 0%(0 / {_fmt_bytes(total)})" if total else "更新下载进度: 已开始")
|
||||
while True:
|
||||
chunk = resp.read(256 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
fp.write(chunk)
|
||||
hasher.update(chunk)
|
||||
downloaded += len(chunk)
|
||||
if total:
|
||||
percent = min(100, downloaded * 100 // total)
|
||||
if percent != last_percent:
|
||||
last_percent = percent
|
||||
log(f"更新下载进度: {percent}%({_fmt_bytes(downloaded)} / {_fmt_bytes(total)})")
|
||||
self._update_download_progress.emit(percent, downloaded, total)
|
||||
|
||||
checksum_text = ""
|
||||
if self._checksum_url:
|
||||
checksum_text = fetch(self._checksum_url).decode("ascii", "replace")
|
||||
checksum_url = urllib.parse.quote(
|
||||
self._checksum_url, safe=':/?#[]@!$&\'()*+,;=%')
|
||||
with opener.open(urllib.request.Request(checksum_url), timeout=120) as resp:
|
||||
checksum_text = resp.read().decode("ascii", "replace")
|
||||
finally:
|
||||
_release_forwarder(started_here)
|
||||
|
||||
if len(data) < 1024:
|
||||
self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE")
|
||||
return
|
||||
if self._asset_size and len(data) != self._asset_size:
|
||||
self._update_download_done.emit(
|
||||
False, f"下载不完整:实际 {len(data)} 字节,应为 {self._asset_size} 字节")
|
||||
return
|
||||
if downloaded < 1024:
|
||||
raise RuntimeError("下载文件过小,可能不是有效的 EXE")
|
||||
if self._asset_size and downloaded != self._asset_size:
|
||||
raise RuntimeError(
|
||||
f"下载不完整:实际 {downloaded} 字节,应为 {self._asset_size} 字节")
|
||||
if checksum_text:
|
||||
want = checksum_text.strip().split()[0].lower()
|
||||
got = hashlib.sha256(data).hexdigest()
|
||||
got = hasher.hexdigest()
|
||||
if len(want) != 64 or want != got:
|
||||
self._update_download_done.emit(False, "更新包校验失败(SHA-256 不匹配),已放弃")
|
||||
return
|
||||
raise RuntimeError("更新包校验失败(SHA-256 不匹配),已放弃")
|
||||
log("更新包 SHA-256 校验通过")
|
||||
|
||||
save_path.write_bytes(data)
|
||||
part_path.replace(save_path)
|
||||
log(f"新版本已下载: {save_path}")
|
||||
self._update_download_done.emit(True, str(save_path))
|
||||
except Exception as e:
|
||||
try:
|
||||
if 'part_path' in locals():
|
||||
part_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
log(f"下载更新失败: {e}")
|
||||
self._update_download_done.emit(False, str(e))
|
||||
|
||||
def _on_update_download_done(self, success, message):
|
||||
"""下载完成后提示用户手动替换 EXE。"""
|
||||
dialog = self._update_progress_dialog
|
||||
self._update_progress_dialog = None
|
||||
if dialog is not None:
|
||||
dialog.close()
|
||||
dialog.deleteLater()
|
||||
if success:
|
||||
QMessageBox.information(
|
||||
self, "下载完成",
|
||||
f"新版本已下载到:\n{message}\n\n请关闭本程序后,用该文件替换当前运行的程序,然后重新启动。"
|
||||
f"新版本已下载到当前程序所在文件夹:\n{message}\n\n请关闭本程序后,用该文件替换当前运行的程序,然后重新启动。"
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
|
||||
|
||||
Reference in New Issue
Block a user