diff --git a/source/core/download_manager.py b/source/core/download_manager.py index f6e649c..67557e4 100644 --- a/source/core/download_manager.py +++ b/source/core/download_manager.py @@ -7,7 +7,7 @@ import re # Added for recursive search from PySide6 import QtWidgets, QtCore from PySide6.QtCore import Qt -from PySide6.QtGui import QIcon, QPixmap +from PySide6.QtGui import QIcon, QPixmap, QFont from utils import msgbox_frame, HostsManager, resource_path from data.config import APP_NAME, PLUGIN, GAME_INFO, UA, CONFIG_URL @@ -264,8 +264,21 @@ class DownloadManager: layout = QVBoxLayout(dialog) - # 添加说明标签 - info_label = QLabel(f"请选择要安装补丁的游戏版本:\n{status_message}", dialog) + # 先显示已安装补丁的游戏 + if already_installed_games: + already_installed_label = QLabel("已安装补丁的游戏:", dialog) + already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold)) + layout.addWidget(already_installed_label) + + already_installed_list = QLabel(chr(10).join(already_installed_games), dialog) + layout.addWidget(already_installed_list) + + # 添加一些间距 + layout.addSpacing(10) + + # 添加"请选择你需要安装补丁的游戏"的标签 + info_label = QLabel("请选择你需要安装补丁的游戏:", dialog) + info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold)) layout.addWidget(info_label) # 添加列表控件 @@ -552,8 +565,9 @@ class DownloadManager: timer.stop() # 如果用户点击了取消安装 - if result == QtWidgets.QMessageBox.StandardButton.RejectRole: + if msg_box.clickedButton() == cancel_button: # 恢复主窗口状态 + self.main_window.setEnabled(True) self.main_window.ui.start_install_text.setText("开始安装") # 清空下载队列 self.download_queue.clear() @@ -604,7 +618,7 @@ class DownloadManager: timer.stop() # 如果用户点击了取消安装 - if result == QtWidgets.QMessageBox.StandardButton.RejectRole: + if msg_box.clickedButton() == cancel_button: # 恢复主窗口状态 self.main_window.setEnabled(True) self.main_window.ui.start_install_text.setText("开始安装") @@ -661,89 +675,9 @@ class DownloadManager: print(f"DEBUG: 准备下载游戏 {game_version}") print(f"DEBUG: 游戏文件夹: {game_folder}") - # 获取游戏可执行文件路径 - game_dirs = self.main_window.game_detector.identify_game_directories_improved(self.selected_folder) - game_exe_exists = False - - if game_version in game_dirs: - game_dir = game_dirs[game_version] - # 游戏目录已经通过可执行文件验证了,可以直接认为存在 - game_exe_exists = True - if debug_mode: - print(f"DEBUG: 游戏目录已验证: {game_dir}") - print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}") - else: - # 回退到传统方法检查游戏是否存在 - # 尝试多种可能的文件名格式 - expected_exe = GAME_INFO[game_version]["exe"] - traditional_folder = os.path.join( - self.selected_folder, - GAME_INFO[game_version]["install_path"].split("/")[0] - ) - - # 定义多种可能的可执行文件变体 - exe_variants = [ - expected_exe, # 标准文件名 - expected_exe + ".nocrack", # Steam加密版本 - expected_exe.replace(".exe", ""), # 无扩展名版本 - expected_exe.replace("NEKOPARA", "nekopara").lower(), # 全小写变体 - expected_exe.lower(), # 小写变体 - expected_exe.lower() + ".nocrack", # 小写变体的Steam加密版本 - ] - - # 对于Vol.3可能有特殊名称 - if "Vol.3" in game_version: - # 增加可能的卷3特定的变体 - exe_variants.extend([ - "NEKOPARAVol3.exe", - "NEKOPARAVol3.exe.nocrack", - "nekoparavol3.exe", - "nekoparavol3.exe.nocrack", - "nekopara_vol3.exe", - "nekopara_vol3.exe.nocrack", - "vol3.exe", - "vol3.exe.nocrack" - ]) - - # 检查所有可能的文件名 - for exe_variant in exe_variants: - exe_path = os.path.join(traditional_folder, exe_variant) - if os.path.exists(exe_path): - game_exe_exists = True - if debug_mode: - print(f"DEBUG: 找到游戏可执行文件: {exe_path}") - break - - # 如果仍未找到,尝试递归搜索 - if not game_exe_exists and os.path.exists(traditional_folder): - # 提取卷号或检查是否是After - vol_match = re.search(r"Vol\.(\d+)", game_version) - vol_num = None - if vol_match: - vol_num = vol_match.group(1) - - is_after = "After" in game_version - - # 遍历游戏目录及其子目录 - for root, dirs, files in os.walk(traditional_folder): - for file in files: - file_lower = file.lower() - if file.endswith('.exe') or file.endswith('.exe.nocrack'): - # 检查文件名中是否包含卷号或关键词 - if ((vol_num and (f"vol{vol_num}" in file_lower or - f"vol.{vol_num}" in file_lower or - f"vol {vol_num}" in file_lower)) or - (is_after and "after" in file_lower)): - game_exe_exists = True - if debug_mode: - print(f"DEBUG: 通过递归搜索找到游戏可执行文件: {os.path.join(root, file)}") - break - if game_exe_exists: - break - - if debug_mode: - print(f"DEBUG: 使用传统方法检查游戏目录: {traditional_folder}") - print(f"DEBUG: 游戏可执行文件存在: {game_exe_exists}") + # 游戏可执行文件已在填充下载队列时验证过,不需要再次检查 + # 因为game_folder是从已验证的game_dirs中获取的 + game_exe_exists = True # 检查游戏是否已安装 if ( diff --git a/source/core/patch_manager.py b/source/core/patch_manager.py index 2e9d661..6d41f42 100644 --- a/source/core/patch_manager.py +++ b/source/core/patch_manager.py @@ -55,26 +55,29 @@ class PatchManager: return self.installed_status.get(game_version, False) return self.installed_status - def uninstall_patch(self, game_dir, game_version): + def uninstall_patch(self, game_dir, game_version, silent=False): """卸载补丁 Args: game_dir: 游戏目录路径 game_version: 游戏版本 + silent: 是否静默模式(不显示弹窗) Returns: bool: 卸载成功返回True,失败返回False + dict: 在silent=True时,返回包含卸载结果信息的字典 """ debug_mode = self._is_debug_mode() if game_version not in self.game_info: - QMessageBox.critical( - None, - f"错误 - {self.app_name}", - f"\n无法识别游戏版本: {game_version}\n", - QMessageBox.StandardButton.Ok, - ) - return False + if not silent: + QMessageBox.critical( + None, + f"错误 - {self.app_name}", + f"\n无法识别游戏版本: {game_version}\n", + QMessageBox.StandardButton.Ok, + ) + return False if not silent else {"success": False, "message": f"无法识别游戏版本: {game_version}", "files_removed": 0} if debug_mode: print(f"DEBUG: 开始卸载 {game_version} 补丁,目录: {game_dir}") @@ -173,8 +176,8 @@ class PatchManager: # 更新安装状态 self.installed_status[game_version] = False - # 在非批量卸载模式下显示卸载成功消息 - if game_version != "all": + # 在非静默模式且非批量卸载模式下显示卸载成功消息 + if not silent and game_version != "all": # 显示卸载成功消息 if files_removed > 0: QMessageBox.information( @@ -192,11 +195,13 @@ class PatchManager: ) # 卸载成功 + if silent: + return {"success": True, "message": f"{game_version} 补丁卸载成功", "files_removed": files_removed} return True except Exception as e: - # 在非批量卸载模式下显示卸载失败消息 - if game_version != "all": + # 在非静默模式且非批量卸载模式下显示卸载失败消息 + if not silent and game_version != "all": # 显示卸载失败消息 error_message = f"\n卸载 {game_version} 补丁时出错:\n\n{str(e)}\n" if debug_mode: @@ -210,6 +215,8 @@ class PatchManager: ) # 卸载失败 + if silent: + return {"success": False, "message": f"卸载 {game_version} 补丁时出错: {str(e)}", "files_removed": 0} return False def batch_uninstall_patches(self, game_dirs): @@ -219,35 +226,166 @@ class PatchManager: game_dirs: 游戏版本到游戏目录的映射字典 Returns: - tuple: (成功数量, 失败数量) + tuple: (成功数量, 失败数量, 详细结果列表) """ success_count = 0 fail_count = 0 debug_mode = self._is_debug_mode() + results = [] for version, path in game_dirs.items(): try: - if self.uninstall_patch(path, version): - success_count += 1 - else: - fail_count += 1 + # 在批量模式下使用静默卸载 + result = self.uninstall_patch(path, version, silent=True) + + if isinstance(result, dict): # 使用了静默模式 + if result["success"]: + success_count += 1 + else: + fail_count += 1 + results.append({ + "version": version, + "success": result["success"], + "message": result["message"], + "files_removed": result["files_removed"] + }) + else: # 兼容旧代码,不应该执行到这里 + if result: + success_count += 1 + else: + fail_count += 1 + results.append({ + "version": version, + "success": result, + "message": f"{version} 卸载{'成功' if result else '失败'}", + "files_removed": 0 + }) + except Exception as e: if debug_mode: print(f"DEBUG: 卸载 {version} 时出错: {str(e)}") fail_count += 1 + results.append({ + "version": version, + "success": False, + "message": f"卸载出错: {str(e)}", + "files_removed": 0 + }) - return success_count, fail_count + return success_count, fail_count, results - def show_uninstall_result(self, success_count, fail_count): + def show_uninstall_result(self, success_count, fail_count, results=None): """显示批量卸载结果 Args: success_count: 成功卸载的数量 fail_count: 卸载失败的数量 + results: 详细结果列表,如果提供,会显示更详细的信息 """ + result_text = f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n" + + # 如果有详细结果,添加到消息中 + if results: + success_list = [r["version"] for r in results if r["success"]] + fail_list = [r["version"] for r in results if not r["success"]] + + if success_list: + result_text += f"\n【成功卸载】:\n{chr(10).join(success_list)}\n" + + if fail_list: + result_text += f"\n【卸载失败】:\n{chr(10).join(fail_list)}\n" + QMessageBox.information( None, f"批量卸载完成 - {self.app_name}", - f"\n批量卸载完成!\n成功: {success_count} 个\n失败: {fail_count} 个\n", + result_text, QMessageBox.StandardButton.Ok, - ) \ No newline at end of file + ) + + def check_patch_installed(self, game_dir, game_version): + """检查游戏是否已安装补丁 + + Args: + game_dir: 游戏目录路径 + game_version: 游戏版本 + + Returns: + bool: 如果已安装补丁返回True,否则返回False + """ + debug_mode = self._is_debug_mode() + + if game_version not in self.game_info: + return False + + # 获取可能的补丁文件路径 + install_path_base = os.path.basename(self.game_info[game_version]["install_path"]) + patch_file_path = os.path.join(game_dir, install_path_base) + + # 尝试查找补丁文件,支持不同大小写 + patch_files_to_check = [ + patch_file_path, + patch_file_path.lower(), + patch_file_path.upper(), + patch_file_path.replace("_", ""), + patch_file_path.replace("_", "-"), + ] + + # 查找补丁文件 + for patch_path in patch_files_to_check: + if os.path.exists(patch_path): + if debug_mode: + print(f"DEBUG: 找到补丁文件: {patch_path}") + return True + + # 检查是否有补丁文件夹 + patch_folders_to_check = [ + os.path.join(game_dir, "patch"), + os.path.join(game_dir, "Patch"), + os.path.join(game_dir, "PATCH"), + ] + + for patch_folder in patch_folders_to_check: + if os.path.exists(patch_folder): + if debug_mode: + print(f"DEBUG: 找到补丁文件夹: {patch_folder}") + return True + + # 检查game/patch文件夹 + game_folders = ["game", "Game", "GAME"] + patch_folders = ["patch", "Patch", "PATCH"] + + for game_folder in game_folders: + for patch_folder in patch_folders: + game_patch_folder = os.path.join(game_dir, game_folder, patch_folder) + if os.path.exists(game_patch_folder): + if debug_mode: + print(f"DEBUG: 找到game/patch文件夹: {game_patch_folder}") + return True + + # 检查配置文件 + config_files = ["config.json", "Config.json", "CONFIG.JSON"] + script_files = ["scripts.json", "Scripts.json", "SCRIPTS.JSON"] + + for game_folder in game_folders: + game_path = os.path.join(game_dir, game_folder) + if os.path.exists(game_path): + # 检查配置文件 + for config_file in config_files: + config_path = os.path.join(game_path, config_file) + if os.path.exists(config_path): + if debug_mode: + print(f"DEBUG: 找到配置文件: {config_path}") + return True + + # 检查脚本文件 + for script_file in script_files: + script_path = os.path.join(game_path, script_file) + if os.path.exists(script_path): + if debug_mode: + print(f"DEBUG: 找到脚本文件: {script_path}") + return True + + # 没有找到补丁文件或文件夹 + if debug_mode: + print(f"DEBUG: {game_version} 在 {game_dir} 中没有安装补丁") + return False \ No newline at end of file diff --git a/source/data/config.py b/source/data/config.py index eaea795..82e533e 100644 --- a/source/data/config.py +++ b/source/data/config.py @@ -3,7 +3,7 @@ import base64 # 配置信息 app_data = { - "APP_VERSION": "1.2.0", + "APP_VERSION": "1.3.0", "APP_NAME": "FRAISEMOE Addons Installer NEXT", "TEMP": "TEMP", "CACHE": "FRAISEMOE", diff --git a/source/main_window.py b/source/main_window.py index cf3ddb8..030b46a 100644 --- a/source/main_window.py +++ b/source/main_window.py @@ -7,7 +7,7 @@ import webbrowser from PySide6 import QtWidgets from PySide6.QtCore import QTimer, Qt, QPoint, QRect, QSize from PySide6.QtWidgets import QMainWindow, QMessageBox, QGraphicsOpacityEffect, QGraphicsColorizeEffect -from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion +from PySide6.QtGui import QPalette, QColor, QPainterPath, QRegion, QFont from PySide6.QtGui import QAction # Added for menu actions from ui.Ui_install import Ui_MainWindows @@ -372,26 +372,23 @@ class MainWindow(QMainWindow): # 构建结果信息 result_text = f"\n安装结果:\n" - # 总数统计 + # 总数统计 - 不再显示已跳过的数量 total_installed = len(installed_versions) - total_skipped = len(skipped_versions) total_failed = len(failed_versions) + total_not_found = len(not_found_versions) - result_text += f"安装成功:{total_installed} 个 已跳过:{total_skipped} 个 安装失败:{total_failed} 个\n\n" + result_text += f"安装成功:{total_installed} 个 安装失败:{total_failed} 个\n\n" # 详细列表 if installed_versions: result_text += f"【成功安装】:\n{chr(10).join(installed_versions)}\n\n" - if skipped_versions: - result_text += f"【已安装跳过】:\n{chr(10).join(skipped_versions)}\n\n" - if failed_versions: result_text += f"【安装失败】:\n{chr(10).join(failed_versions)}\n\n" - if not_found_versions and (installed_versions or failed_versions): - # 只有当有其他版本存在时,才显示未找到的版本 - result_text += f"【未在指定目录找到】:\n{chr(10).join(not_found_versions)}\n" + if not_found_versions: + # 将"未在指定目录找到"改为"尚未安装补丁的游戏" + result_text += f"【尚未安装补丁的游戏】:\n{chr(10).join(not_found_versions)}\n" QMessageBox.information( self, @@ -528,47 +525,110 @@ class MainWindow(QMainWindow): game_dirs = self.game_detector.identify_game_directories_improved(selected_folder) if game_dirs and len(game_dirs) > 0: - # 找到了游戏目录,显示选择对话框 if debug_mode: print(f"DEBUG: 卸载功能 - 在上级目录中找到以下游戏: {list(game_dirs.keys())}") - # 如果只有一个游戏,直接选择它 - if len(game_dirs) == 1: - game_version = list(game_dirs.keys())[0] - game_dir = game_dirs[game_version] - self._confirm_and_uninstall(game_dir, game_version) - else: - # 有多个游戏,让用户选择 - from PySide6.QtWidgets import QInputDialog - game_versions = list(game_dirs.keys()) - # 添加"全部卸载"选项 - game_versions.append("全部卸载") - - selected_game, ok = QInputDialog.getItem( - self, "选择游戏", "选择要卸载补丁的游戏:", - game_versions, 0, False + # 查找已安装补丁的游戏,只处理那些已安装补丁的游戏 + games_with_patch = {} + for game_version, game_dir in game_dirs.items(): + if self.patch_manager.check_patch_installed(game_dir, game_version): + games_with_patch[game_version] = game_dir + if debug_mode: + print(f"DEBUG: 卸载功能 - {game_version} 已安装补丁") + + # 检查是否有已安装补丁的游戏 + if not games_with_patch: + QMessageBox.information( + self, + f"提示 - {APP_NAME}", + "\n未在选择的目录中找到已安装补丁的游戏。\n请确认您选择了正确的游戏目录,并且该目录中的游戏已安装过补丁。\n", + QMessageBox.StandardButton.Ok ) + return + + # 创建自定义选择对话框,允许多选 + from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout, QAbstractItemView + + dialog = QDialog(self) + dialog.setWindowTitle("选择要卸载的游戏补丁") + dialog.resize(400, 300) + + layout = QVBoxLayout(dialog) + + # 添加"已安装补丁的游戏"标签 + already_installed_label = QLabel("已安装补丁的游戏:", dialog) + already_installed_label.setFont(QFont(already_installed_label.font().family(), already_installed_label.font().pointSize(), QFont.Bold)) + layout.addWidget(already_installed_label) + + # 添加已安装游戏列表(可选,这里使用静态标签替代,保持一致性) + installed_games_text = ", ".join(games_with_patch.keys()) + installed_games_label = QLabel(installed_games_text, dialog) + layout.addWidget(installed_games_label) + + # 添加一些间距 + layout.addSpacing(10) + + # 添加"请选择要卸载补丁的游戏"标签 + info_label = QLabel("请选择要卸载补丁的游戏:", dialog) + info_label.setFont(QFont(info_label.font().family(), info_label.font().pointSize(), QFont.Bold)) + layout.addWidget(info_label) + + # 添加列表控件,只显示已安装补丁的游戏 + list_widget = QListWidget(dialog) + list_widget.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # 允许多选 + for game in games_with_patch.keys(): + list_widget.addItem(game) + layout.addWidget(list_widget) + + # 添加全选按钮 + select_all_btn = QPushButton("全选", dialog) + select_all_btn.clicked.connect(lambda: list_widget.selectAll()) + layout.addWidget(select_all_btn) + + # 添加确定和取消按钮 + buttons_layout = QHBoxLayout() + ok_button = QPushButton("确定", dialog) + cancel_button = QPushButton("取消", dialog) + buttons_layout.addWidget(ok_button) + buttons_layout.addWidget(cancel_button) + layout.addLayout(buttons_layout) + + # 连接按钮事件 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框并等待用户选择 + result = dialog.exec() + + if result != QDialog.DialogCode.Accepted or list_widget.selectedItems() == []: + # 用户取消或未选择任何游戏 + return - if ok and selected_game: - if selected_game == "全部卸载": - # 卸载所有游戏补丁 - reply = QMessageBox.question( - self, - f"确认卸载 - {APP_NAME}", - f"\n确定要卸载所有游戏的补丁吗?\n这将卸载以下游戏的补丁:\n{chr(10).join(list(game_dirs.keys()))}\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - - if reply == QMessageBox.StandardButton.Yes: - # 使用批量卸载方法 - success_count, fail_count = self.patch_manager.batch_uninstall_patches(game_dirs) - self.patch_manager.show_uninstall_result(success_count, fail_count) - else: - # 卸载选中的单个游戏 - game_version = selected_game - game_dir = game_dirs[game_version] - self._confirm_and_uninstall(game_dir, game_version) + # 获取用户选择的游戏 + selected_games = [item.text() for item in list_widget.selectedItems()] + if debug_mode: + print(f"DEBUG: 卸载功能 - 用户选择了以下游戏进行卸载: {selected_games}") + + # 过滤game_dirs,只保留选中的游戏 + selected_game_dirs = {game: games_with_patch[game] for game in selected_games if game in games_with_patch} + + # 确认卸载 + game_list = '\n'.join(selected_games) + reply = QMessageBox.question( + self, + f"确认卸载 - {APP_NAME}", + f"\n确定要卸载以下游戏的补丁吗?\n\n{game_list}\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.No: + return + + # 使用批量卸载方法 + success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(selected_game_dirs) + self.patch_manager.show_uninstall_result(success_count, fail_count, results) + else: # 未找到游戏目录,尝试将选择的目录作为游戏目录 if debug_mode: @@ -579,7 +639,31 @@ class MainWindow(QMainWindow): if game_version: if debug_mode: print(f"DEBUG: 卸载功能 - 识别为游戏: {game_version}") - self._confirm_and_uninstall(selected_folder, game_version) + + # 检查是否已安装补丁 + if self.patch_manager.check_patch_installed(selected_folder, game_version): + # 确认卸载 + reply = QMessageBox.question( + self, + f"确认卸载 - {APP_NAME}", + f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {selected_folder}\n", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + # 创建单个游戏的目录字典,使用批量卸载流程 + single_game_dir = {game_version: selected_folder} + success_count, fail_count, results = self.patch_manager.batch_uninstall_patches(single_game_dir) + self.patch_manager.show_uninstall_result(success_count, fail_count, results) + else: + # 没有安装补丁 + QMessageBox.information( + self, + f"提示 - {APP_NAME}", + f"\n未在 {game_version} 中找到已安装的补丁。\n请确认该游戏已经安装过补丁。\n", + QMessageBox.StandardButton.Ok + ) else: # 两种方式都未识别到游戏 if debug_mode: @@ -592,28 +676,6 @@ class MainWindow(QMainWindow): ) msg_box.exec() - def _confirm_and_uninstall(self, game_dir, game_version): - """确认并卸载补丁 - - Args: - game_dir: 游戏目录 - game_version: 游戏版本 - """ - debug_mode = self.debug_manager._is_debug_mode() - - # 确认卸载 - reply = QMessageBox.question( - self, - f"确认卸载 - {APP_NAME}", - f"\n确定要卸载 {game_version} 的补丁吗?\n游戏目录: {game_dir}\n", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - - if reply == QMessageBox.StandardButton.No: - return - - # 开始卸载补丁 - self.patch_manager.uninstall_patch(game_dir, game_version) + \ No newline at end of file diff --git a/source/workers/download.py b/source/workers/download.py index 7d77f1b..52c85f5 100644 --- a/source/workers/download.py +++ b/source/workers/download.py @@ -9,6 +9,25 @@ from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBox from utils import resource_path from data.config import APP_NAME, UA import signal +import ctypes + +# Windows API常量和函数 +if sys.platform == 'win32': + kernel32 = ctypes.windll.kernel32 + PROCESS_ALL_ACCESS = 0x1F0FFF + THREAD_SUSPEND_RESUME = 0x0002 + TH32CS_SNAPTHREAD = 0x00000004 + + class THREADENTRY32(ctypes.Structure): + _fields_ = [ + ('dwSize', ctypes.c_ulong), + ('cntUsage', ctypes.c_ulong), + ('th32ThreadID', ctypes.c_ulong), + ('th32OwnerProcessID', ctypes.c_ulong), + ('tpBasePri', ctypes.c_ulong), + ('tpDeltaPri', ctypes.c_ulong), + ('dwFlags', ctypes.c_ulong) + ] # 下载线程类 class DownloadThread(QThread): @@ -23,7 +42,7 @@ class DownloadThread(QThread): self.process = None self._is_running = True self._is_paused = False - self.pause_process = None + self.threads = [] def stop(self): if self.process and self.process.poll() is None: @@ -34,24 +53,56 @@ class DownloadThread(QThread): except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"停止下载进程时出错: {e}") + def _get_process_threads(self, pid): + """获取进程的所有线程ID""" + if sys.platform != 'win32': + return [] + + thread_ids = [] + h_snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) + if h_snapshot == -1: + return [] + + thread_entry = THREADENTRY32() + thread_entry.dwSize = ctypes.sizeof(THREADENTRY32) + + res = kernel32.Thread32First(h_snapshot, ctypes.byref(thread_entry)) + while res: + if thread_entry.th32OwnerProcessID == pid: + thread_ids.append(thread_entry.th32ThreadID) + res = kernel32.Thread32Next(h_snapshot, ctypes.byref(thread_entry)) + + kernel32.CloseHandle(h_snapshot) + return thread_ids + def pause(self): """暂停下载进程""" if not self._is_paused and self.process and self.process.poll() is None: try: - # 使用SIGSTOP信号暂停进程 - # Windows下使用不同的方式,因为没有SIGSTOP if sys.platform == 'win32': + # 获取所有线程 + self.threads = self._get_process_threads(self.process.pid) + if not self.threads: + print("未找到可暂停的线程") + return False + + # 暂停所有线程 + for thread_id in self.threads: + h_thread = kernel32.OpenThread(THREAD_SUSPEND_RESUME, False, thread_id) + if h_thread: + kernel32.SuspendThread(h_thread) + kernel32.CloseHandle(h_thread) + self._is_paused = True - # 在Windows上,使用暂停进程的方法 - self.pause_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Suspend()'], - creationflags=subprocess.CREATE_NO_WINDOW) + print(f"下载进程已暂停: PID {self.process.pid}, 线程数: {len(self.threads)}") + return True else: # 在Unix系统上使用SIGSTOP os.kill(self.process.pid, signal.SIGSTOP) self._is_paused = True - print(f"下载进程已暂停: PID {self.process.pid}") - return True - except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e: + print(f"下载进程已暂停: PID {self.process.pid}") + return True + except Exception as e: print(f"暂停下载进程时出错: {e}") return False return False @@ -60,21 +111,24 @@ class DownloadThread(QThread): """恢复下载进程""" if self._is_paused and self.process and self.process.poll() is None: try: - # 使用SIGCONT信号恢复进程 - # Windows下使用不同的方式 if sys.platform == 'win32': + # 恢复所有线程 + for thread_id in self.threads: + h_thread = kernel32.OpenThread(THREAD_SUSPEND_RESUME, False, thread_id) + if h_thread: + kernel32.ResumeThread(h_thread) + kernel32.CloseHandle(h_thread) + self._is_paused = False - # 在Windows上,使用恢复进程的方法 - resume_process = subprocess.Popen(['powershell', '-Command', f'(Get-Process -Id {self.process.pid}).Resume()'], - creationflags=subprocess.CREATE_NO_WINDOW) - resume_process.wait() + print(f"下载进程已恢复: PID {self.process.pid}, 线程数: {len(self.threads)}") + return True else: # 在Unix系统上使用SIGCONT os.kill(self.process.pid, signal.SIGCONT) self._is_paused = False - print(f"下载进程已恢复: PID {self.process.pid}") - return True - except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e: + print(f"下载进程已恢复: PID {self.process.pid}") + return True + except Exception as e: print(f"恢复下载进程时出错: {e}") return False return False