feat(core): 优化线程管理和清理机制

- 在主窗口中添加优雅的线程清理逻辑,确保在退出时安全停止所有后台线程,避免潜在的资源泄漏。
- 更新离线模式管理器和哈希线程,增强对线程引用的管理,确保在操作完成后及时清理引用。
- 改进补丁检测器,支持在离线模式下的补丁状态检查,提升用户体验和系统稳定性。
- 增强日志记录,确保在关键操作中提供详细的调试信息,便于后续排查和用户反馈。
This commit is contained in:
hyb-oyqq
2025-08-11 14:42:38 +08:00
parent f0031ed17c
commit 6a4c6ca1f1
57 changed files with 8518 additions and 237 deletions

View File

@@ -1,7 +1,7 @@
import os
import shutil
import py7zr
from PySide6.QtCore import QThread, Signal, QCoreApplication
from PySide6.QtCore import QThread, Signal
from data.config import PLUGIN, GAME_INFO
class ExtractionThread(QThread):
@@ -20,33 +20,37 @@ class ExtractionThread(QThread):
try:
# 确保游戏目录存在
os.makedirs(self.game_folder, exist_ok=True)
# 发送初始进度信号
self.progress.emit(0, f"开始处理 {self.game_version} 的补丁文件...")
# 确保UI更新
QCoreApplication.processEvents()
def update_progress(percent: int, message: str):
try:
self.progress.emit(percent, message)
except Exception:
pass
update_progress(0, f"开始处理 {self.game_version} 的补丁文件...")
# 支持外部请求中断
if self.isInterruptionRequested():
self.finished.emit(False, "操作已取消", self.game_version)
return
# 如果提供了已解压文件路径,直接使用它
if self.extracted_path and os.path.exists(self.extracted_path):
# 发送进度信号
self.progress.emit(20, f"正在复制 {self.game_version} 的补丁文件...")
QCoreApplication.processEvents()
update_progress(20, f"正在复制 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
# 直接复制已解压的文件到游戏目录
target_file = os.path.join(self.game_folder, os.path.basename(self.plugin_path))
shutil.copy(self.extracted_path, target_file)
# 发送进度信号
self.progress.emit(60, f"正在完成 {self.game_version} 的补丁安装...")
QCoreApplication.processEvents()
update_progress(60, f"正在完成 {self.game_version} 的补丁安装...")
# 对于NEKOPARA After还需要复制签名文件
if self.game_version == "NEKOPARA After":
# 从已解压文件的目录中获取签名文件
extracted_dir = os.path.dirname(self.extracted_path)
sig_filename = os.path.basename(GAME_INFO[self.game_version]["sig_path"])
sig_path = os.path.join(extracted_dir, sig_filename)
# 如果签名文件存在,则复制它
if os.path.exists(sig_path):
shutil.copy(sig_path, self.game_folder)
@@ -54,93 +58,79 @@ class ExtractionThread(QThread):
# 如果签名文件不存在,则使用原始路径
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
shutil.copy(sig_path, self.game_folder)
# 发送完成进度信号
self.progress.emit(100, f"{self.game_version} 补丁文件处理完成")
QCoreApplication.processEvents()
else:
# 如果没有提供已解压文件路径,直接解压到游戏目录
# 获取目标文件名
target_filename = os.path.basename(self.plugin_path)
target_path = os.path.join(self.game_folder, target_filename)
# 发送进度信号
self.progress.emit(10, f"正在打开 {self.game_version} 的补丁压缩包...")
QCoreApplication.processEvents()
# 使用7z解压
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
# 获取压缩包内的文件列表
file_list = archive.getnames()
# 发送进度信号
self.progress.emit(20, f"正在分析 {self.game_version} 的补丁文件...")
QCoreApplication.processEvents()
# 解析压缩包内的文件结构
target_file_in_archive = None
for file_path in file_list:
if target_filename in file_path:
target_file_in_archive = file_path
break
if not target_file_in_archive:
raise FileNotFoundError(f"在压缩包中未找到目标文件 {target_filename}")
# 发送进度信号
self.progress.emit(30, f"正在解压 {self.game_version} 的补丁文件...")
QCoreApplication.processEvents()
# 创建一个临时目录用于解压单个文件
import tempfile
with tempfile.TemporaryDirectory() as temp_dir:
# 解压特定文件到临时目录
archive.extract(path=temp_dir, targets=[target_file_in_archive])
# 发送进度信号
self.progress.emit(60, f"正在复制 {self.game_version} 的补丁文件...")
QCoreApplication.processEvents()
# 找到解压后的文件
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
# 复制到目标位置
shutil.copy2(extracted_file_path, target_path)
# 发送进度信号
self.progress.emit(80, f"正在完成 {self.game_version} 的补丁安装...")
QCoreApplication.processEvents()
# 对于NEKOPARA After还需要复制签名文件
if self.game_version == "NEKOPARA After":
sig_filename = f"{target_filename}.sig"
sig_file_in_archive = None
# 查找签名文件
for file_path in file_list:
if sig_filename in file_path:
sig_file_in_archive = file_path
break
if sig_file_in_archive:
# 解压签名文件
archive.extract(path=temp_dir, targets=[sig_file_in_archive])
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
update_progress(100, f"{self.game_version} 补丁文件处理完成")
self.finished.emit(True, "", self.game_version)
return
# 否则解压源压缩包到临时目录,再复制目标文件
target_filename = os.path.basename(self.plugin_path)
target_path = os.path.join(self.game_folder, target_filename)
update_progress(10, f"正在打开 {self.game_version} 的补丁压缩包...")
with py7zr.SevenZipFile(self._7z_path, mode="r") as archive:
# 获取压缩包内的文件列表
file_list = archive.getnames()
update_progress(20, f"正在分析 {self.game_version} 的补丁文件...")
# 查找压缩包内的目标文件
target_file_in_archive = None
for file_path in file_list:
if target_filename in file_path:
target_file_in_archive = file_path
break
if not target_file_in_archive:
raise FileNotFoundError(f"在压缩包中未找到目标文件 {target_filename}")
update_progress(30, f"正在解压 {self.game_version} 的补丁文件...\n(在此过程中可能会卡顿或无响应,请不要关闭软件)")
import tempfile
with tempfile.TemporaryDirectory() as temp_dir:
# 解压特定文件到临时目录
archive.extract(path=temp_dir, targets=[target_file_in_archive])
update_progress(60, f"正在复制 {self.game_version} 的补丁文件...")
# 找到解压后的文件
extracted_file_path = os.path.join(temp_dir, target_file_in_archive)
# 复制到目标位置
shutil.copy2(extracted_file_path, target_path)
update_progress(80, f"正在完成 {self.game_version} 的补丁安装...")
# 对于NEKOPARA After还需要复制签名文件
if self.game_version == "NEKOPARA After":
sig_filename = f"{target_filename}.sig"
sig_file_in_archive = None
# 查找签名文件
for file_path in file_list:
if sig_filename in file_path:
sig_file_in_archive = file_path
break
if sig_file_in_archive:
# 解压签名文件
archive.extract(path=temp_dir, targets=[sig_file_in_archive])
extracted_sig_path = os.path.join(temp_dir, sig_file_in_archive)
sig_target = os.path.join(self.game_folder, sig_filename)
shutil.copy2(extracted_sig_path, sig_target)
else:
# 如果签名文件不存在,则使用原始路径
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
if os.path.exists(sig_path):
sig_target = os.path.join(self.game_folder, sig_filename)
shutil.copy2(extracted_sig_path, sig_target)
else:
# 如果签名文件不存在,则使用原始路径
sig_path = os.path.join(PLUGIN, GAME_INFO[self.game_version]["sig_path"])
if os.path.exists(sig_path):
sig_target = os.path.join(self.game_folder, sig_filename)
shutil.copy2(sig_path, sig_target)
# 发送完成进度信号
self.progress.emit(100, f"{self.game_version} 补丁文件解压完成")
QCoreApplication.processEvents()
shutil.copy2(sig_path, sig_target)
update_progress(100, f"{self.game_version} 补丁文件解压完成")
self.finished.emit(True, "", self.game_version)
except (py7zr.Bad7zFile, FileNotFoundError, Exception) as e:
self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败")
QCoreApplication.processEvents()
try:
self.progress.emit(100, f"处理 {self.game_version} 的补丁文件失败")
except Exception:
pass
self.finished.emit(False, f"\n文件操作失败,请重试\n\n【错误信息】:{e}\n", self.game_version)

View File

@@ -43,6 +43,8 @@ class HashThread(QThread):
status_copy = self.installed_status.copy()
for game_version, install_path in self.install_paths.items():
if self.isInterruptionRequested():
break
if not os.path.exists(install_path):
status_copy[game_version] = False
if debug_mode:
@@ -57,8 +59,17 @@ class HashThread(QThread):
# 当没有预期哈希值时,保持当前状态不变
continue
# 分块读取,避免大文件一次性读取内存
hash_obj = hashlib.sha256()
with open(install_path, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
while True:
if self.isInterruptionRequested():
break
chunk = f.read(1024 * 1024)
if not chunk:
break
hash_obj.update(chunk)
file_hash = hash_obj.hexdigest()
if debug_mode:
logger.debug(f"DEBUG: 哈希预检查 - {game_version}")
@@ -86,6 +97,8 @@ class HashThread(QThread):
result = {"passed": True, "game": "", "message": ""}
for game_version, install_path in self.install_paths.items():
if self.isInterruptionRequested():
break
if not os.path.exists(install_path):
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version} 补丁文件不存在: {install_path}")
@@ -99,8 +112,17 @@ class HashThread(QThread):
# 当没有预期哈希值时,跳过检查
continue
# 分块读取,避免大文件一次性读取内存
hash_obj = hashlib.sha256()
with open(install_path, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
while True:
if self.isInterruptionRequested():
break
chunk = f.read(1024 * 1024)
if not chunk:
break
hash_obj.update(chunk)
file_hash = hash_obj.hexdigest()
if debug_mode:
logger.debug(f"DEBUG: 哈希后检查 - {game_version}")
@@ -167,9 +189,7 @@ class OfflineHashVerifyThread(QThread):
if not expected_hash:
logger.warning(f"DEBUG: 未找到 {self.game_version} 的预期哈希值")
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"未找到 {self.game_version} 的预期哈希值", "")
return
@@ -183,9 +203,7 @@ class OfflineHashVerifyThread(QThread):
if not os.path.exists(self.file_path):
if debug_mode:
logger.warning(f"DEBUG: 补丁文件不存在: {self.file_path}")
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"补丁文件不存在: {self.file_path}", "")
return
@@ -197,9 +215,7 @@ class OfflineHashVerifyThread(QThread):
if file_size == 0:
if debug_mode:
logger.warning(f"DEBUG: 补丁文件大小为0无效文件")
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, "补丁文件大小为0无效文件", "")
return
@@ -233,7 +249,6 @@ class OfflineHashVerifyThread(QThread):
if debug_mode:
logger.warning(f"DEBUG: 未知的游戏版本: {self.game_version}")
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"未知的游戏版本: {self.game_version}", "")
return
@@ -284,7 +299,6 @@ class OfflineHashVerifyThread(QThread):
if debug_mode:
logger.warning(f"DEBUG: 未找到解压后的补丁文件")
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, "未找到解压后的补丁文件", "")
return
else:
@@ -331,9 +345,7 @@ class OfflineHashVerifyThread(QThread):
logger.debug(f"DEBUG: 文件: {files}")
if not os.path.exists(patch_file):
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"未找到解压后的补丁文件", "")
return
@@ -352,7 +364,12 @@ class OfflineHashVerifyThread(QThread):
with open(patch_file, "rb") as f:
bytes_read = 0
while chunk := f.read(chunk_size):
while True:
if self.isInterruptionRequested():
break
chunk = f.read(chunk_size)
if not chunk:
break
hash_obj.update(chunk)
bytes_read += len(chunk)
# 计算进度 (70-95%)
@@ -366,8 +383,6 @@ class OfflineHashVerifyThread(QThread):
# 发送进度信号 - 100%
self.progress.emit(100)
# 确保UI更新
QApplication.processEvents()
if debug_mode:
logger.debug(f"DEBUG: 补丁文件 {patch_file} 哈希值验证: {'成功' if result else '失败'}")
@@ -382,26 +397,20 @@ class OfflineHashVerifyThread(QThread):
if debug_mode:
logger.error(f"DEBUG: 计算补丁文件哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"计算补丁文件哈希值失败: {str(e)}", "")
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 解压补丁文件失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
# 确保发送100%进度信号以便UI更新
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"解压补丁文件失败: {str(e)}", "")
return
except Exception as e:
if debug_mode:
logger.error(f"DEBUG: 验证补丁哈希值失败: {e}")
logger.error(f"DEBUG: 错误类型: {type(e).__name__}")
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}")
# 确保发送100%进度信号以便UI更新
logger.error(f"DEBUG: 错误堆栈: {traceback.format_exc()}" )
self.progress.emit(100)
QApplication.processEvents()
self.finished.emit(False, f"验证补丁哈希值失败: {str(e)}", "")