From 6673d28503570809ee59565c320c80e61a1ea545 Mon Sep 17 00:00:00 2001 From: RoyirinRosmontis Date: Wed, 16 Sep 2026 23:35:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B=EF=BC=9A=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8B=E8=BD=BD=E8=BF=9B=E5=BA=A6=E5=B9=B6?= =?UTF-8?q?=E5=B0=B1=E5=9C=B0=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lite/main.py | 116 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 23 deletions(-) diff --git a/lite/main.py b/lite/main.py index 31c7965..c82ebb9 100644 --- a/lite/main.py +++ b/lite/main.py @@ -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 - try: - data = fetch(url) + 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: + 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}")