改进:显示更新下载进度并就地保存
This commit is contained in:
+93
-23
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QWidget, QLabel, QPushButton,
|
QApplication, QWidget, QLabel, QPushButton,
|
||||||
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
||||||
QCheckBox, QMenu, QDialog,
|
QCheckBox, QMenu, QDialog, QProgressDialog,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||||
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
||||||
@@ -1652,6 +1652,7 @@ class MainWindow(QWidget):
|
|||||||
_op_done = pyqtSignal(str, bool)
|
_op_done = pyqtSignal(str, bool)
|
||||||
_update_done = pyqtSignal()
|
_update_done = pyqtSignal()
|
||||||
_update_download_done = pyqtSignal(bool, str)
|
_update_download_done = pyqtSignal(bool, str)
|
||||||
|
_update_download_progress = pyqtSignal(int, int, int)
|
||||||
|
|
||||||
_status_ready = pyqtSignal(object)
|
_status_ready = pyqtSignal(object)
|
||||||
_node_switched = pyqtSignal() # 启动校验时代替用户切换了节点(刷新标题栏)
|
_node_switched = pyqtSignal() # 启动校验时代替用户切换了节点(刷新标题栏)
|
||||||
@@ -1665,6 +1666,7 @@ class MainWindow(QWidget):
|
|||||||
self._start_in_progress = False
|
self._start_in_progress = False
|
||||||
self._proxy_state_lock = threading.RLock()
|
self._proxy_state_lock = threading.RLock()
|
||||||
self._status_busy = False # 防止状态轮询线程堆叠
|
self._status_busy = False # 防止状态轮询线程堆叠
|
||||||
|
self._update_progress_dialog = None
|
||||||
# 把全局 log() 函数接到 GUI 日志区
|
# 把全局 log() 函数接到 GUI 日志区
|
||||||
global _log_func
|
global _log_func
|
||||||
_log_func = self.log_signal.emit
|
_log_func = self.log_signal.emit
|
||||||
@@ -1672,6 +1674,7 @@ class MainWindow(QWidget):
|
|||||||
self._op_done.connect(self._after_op)
|
self._op_done.connect(self._after_op)
|
||||||
self._update_done.connect(self._on_update_done)
|
self._update_done.connect(self._on_update_done)
|
||||||
self._update_download_done.connect(self._on_update_download_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._status_ready.connect(self._apply_status)
|
||||||
self._node_switched.connect(self._on_node_switched)
|
self._node_switched.connect(self._on_node_switched)
|
||||||
|
|
||||||
@@ -2338,8 +2341,35 @@ class MainWindow(QWidget):
|
|||||||
QMessageBox.Yes,
|
QMessageBox.Yes,
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
|
self._show_update_download_progress()
|
||||||
threading.Thread(target=self._download_update, daemon=True).start()
|
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):
|
def _download_update(self):
|
||||||
"""后台下载新版本 EXE 到 EXE 所在目录。
|
"""后台下载新版本 EXE 到 EXE 所在目录。
|
||||||
|
|
||||||
@@ -2357,58 +2387,98 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
# 对 URL 进行安全编码,兼容空格或已编码空格
|
# 对 URL 进行安全编码,兼容空格或已编码空格
|
||||||
url = urllib.parse.quote(url, safe=':/?#[]@!$&\'()*+,;=%')
|
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"正在下载新版本: {url}")
|
||||||
log(f"保存到: {save_path}")
|
log(f"保存到: {save_path}")
|
||||||
|
|
||||||
def fetch(target):
|
use_proxy = _update_via_proxy()
|
||||||
# 同更新检查:按「使用代理更新」设置选择隧道或直连
|
started_here = _ensure_forwarder() if use_proxy else False
|
||||||
if _update_via_proxy():
|
try:
|
||||||
|
if use_proxy:
|
||||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler(
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler(
|
||||||
{"http": f"{PROXY_HOST}:{PROXY_PORT}",
|
{"http": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
|
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
|
||||||
else:
|
else:
|
||||||
opener = _pac_no_proxy_opener()
|
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():
|
||||||
try:
|
part_path.unlink()
|
||||||
data = fetch(url)
|
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 = ""
|
checksum_text = ""
|
||||||
if self._checksum_url:
|
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:
|
finally:
|
||||||
_release_forwarder(started_here)
|
_release_forwarder(started_here)
|
||||||
|
|
||||||
if len(data) < 1024:
|
if downloaded < 1024:
|
||||||
self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE")
|
raise RuntimeError("下载文件过小,可能不是有效的 EXE")
|
||||||
return
|
if self._asset_size and downloaded != self._asset_size:
|
||||||
if self._asset_size and len(data) != self._asset_size:
|
raise RuntimeError(
|
||||||
self._update_download_done.emit(
|
f"下载不完整:实际 {downloaded} 字节,应为 {self._asset_size} 字节")
|
||||||
False, f"下载不完整:实际 {len(data)} 字节,应为 {self._asset_size} 字节")
|
|
||||||
return
|
|
||||||
if checksum_text:
|
if checksum_text:
|
||||||
want = checksum_text.strip().split()[0].lower()
|
want = checksum_text.strip().split()[0].lower()
|
||||||
got = hashlib.sha256(data).hexdigest()
|
got = hasher.hexdigest()
|
||||||
if len(want) != 64 or want != got:
|
if len(want) != 64 or want != got:
|
||||||
self._update_download_done.emit(False, "更新包校验失败(SHA-256 不匹配),已放弃")
|
raise RuntimeError("更新包校验失败(SHA-256 不匹配),已放弃")
|
||||||
return
|
|
||||||
log("更新包 SHA-256 校验通过")
|
log("更新包 SHA-256 校验通过")
|
||||||
|
|
||||||
save_path.write_bytes(data)
|
part_path.replace(save_path)
|
||||||
log(f"新版本已下载: {save_path}")
|
log(f"新版本已下载: {save_path}")
|
||||||
self._update_download_done.emit(True, str(save_path))
|
self._update_download_done.emit(True, str(save_path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
if 'part_path' in locals():
|
||||||
|
part_path.unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
log(f"下载更新失败: {e}")
|
log(f"下载更新失败: {e}")
|
||||||
self._update_download_done.emit(False, str(e))
|
self._update_download_done.emit(False, str(e))
|
||||||
|
|
||||||
def _on_update_download_done(self, success, message):
|
def _on_update_download_done(self, success, message):
|
||||||
"""下载完成后提示用户手动替换 EXE。"""
|
"""下载完成后提示用户手动替换 EXE。"""
|
||||||
|
dialog = self._update_progress_dialog
|
||||||
|
self._update_progress_dialog = None
|
||||||
|
if dialog is not None:
|
||||||
|
dialog.close()
|
||||||
|
dialog.deleteLater()
|
||||||
if success:
|
if success:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "下载完成",
|
self, "下载完成",
|
||||||
f"新版本已下载到:\n{message}\n\n请关闭本程序后,用该文件替换当前运行的程序,然后重新启动。"
|
f"新版本已下载到当前程序所在文件夹:\n{message}\n\n请关闭本程序后,用该文件替换当前运行的程序,然后重新启动。"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
|
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
|
||||||
|
|||||||
Reference in New Issue
Block a user