import os import sys import subprocess import re from urllib.parse import urlparse from PySide6 import QtCore, QtWidgets from PySide6.QtCore import (Qt, Signal, QThread, QTimer) from PySide6.QtWidgets import (QLabel, QProgressBar, QVBoxLayout, QDialog, QHBoxLayout) from utils import resource_path from data.config import APP_NAME, UA import signal import ctypes import time 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): progress = Signal(dict) finished = Signal(bool, str) def __init__(self, url, _7z_path, game_version, parent=None): super().__init__(parent) self.url = url self._7z_path = _7z_path self.game_version = game_version self.process = None self._is_running = True self._is_paused = False self.threads = [] def stop(self): if self.process and self.process.poll() is None: self._is_running = False try: subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW) 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: 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 print(f"下载进程已暂停: PID {self.process.pid}, 线程数: {len(self.threads)}") return True else: os.kill(self.process.pid, signal.SIGSTOP) self._is_paused = True print(f"下载进程已暂停: PID {self.process.pid}") return True except Exception as e: print(f"暂停下载进程时出错: {e}") return False return False def resume(self): """恢复下载进程""" if self._is_paused and self.process and self.process.poll() is None: try: 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 print(f"下载进程已恢复: PID {self.process.pid}, 线程数: {len(self.threads)}") return True else: os.kill(self.process.pid, signal.SIGCONT) self._is_paused = False print(f"下载进程已恢复: PID {self.process.pid}") return True except Exception as e: print(f"恢复下载进程时出错: {e}") return False return False def is_paused(self): """返回当前下载是否处于暂停状态""" return self._is_paused def run(self): try: if not self._is_running: self.finished.emit(False, "下载已手动停止。") return aria2c_path = resource_path("aria2c-fast_x64.exe") download_dir = os.path.dirname(self._7z_path) file_name = os.path.basename(self._7z_path) parsed_url = urlparse(self.url) referer = f"{parsed_url.scheme}://{parsed_url.netloc}/" command = [ aria2c_path, ] thread_count = 64 # 默认值 if hasattr(self.parent(), 'download_manager'): thread_count = self.parent().download_manager.get_download_thread_count() ipv6_enabled = False if hasattr(self.parent(), 'config'): ipv6_enabled = self.parent().config.get("ipv6_enabled", False) print(f"IPv6支持状态: {ipv6_enabled}") command.extend([ '--dir', download_dir, '--out', file_name, '--user-agent', UA, '--referer', referer, '--header', f'Origin: {referer.rstrip("/")}', '--header', 'Accept: */*', '--header', 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8', '--header', 'Accept-Encoding: gzip, deflate, br', '--header', 'Cache-Control: no-cache', '--header', 'Pragma: no-cache', '--header', 'DNT: 1', '--header', 'Sec-Fetch-Dest: empty', '--header', 'Sec-Fetch-Mode: cors', '--header', 'Sec-Fetch-Site: same-origin', '--http-accept-gzip=true', '--console-log-level=notice', '--summary-interval=1', '--log-level=notice', '--max-tries=3', '--retry-wait=2', '--connect-timeout=60', '--timeout=60', '--auto-file-renaming=false', '--allow-overwrite=true', '--split=128', f'--max-connection-per-server={thread_count}', '--min-split-size=1M', '--optimize-concurrent-downloads=true', '--file-allocation=none', '--async-dns=true', ]) if not ipv6_enabled: command.append('--disable-ipv6=true') print("已禁用IPv6支持") else: print("已启用IPv6支持") command.append('--check-certificate=false') command.append(self.url) print(f"即将执行的 Aria2c 命令: {' '.join(command)}") creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0 self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags) # 正则表达式用于解析aria2c的输出: #1 GID[...]( 5%) CN:1 DL:10.5MiB/s ETA:1m30s progress_pattern = re.compile(r'\((\d{1,3})%\).*?CN:(\d+).*?DL:\s*([^\s]+).*?ETA:\s*([^\s\]]+)') last_update_time = 0 update_interval = 0.2 # 限制UI更新频率,每0.2秒最多更新一次 full_output = [] while self._is_running and self.process.poll() is None: if self.process.stdout: line = self.process.stdout.readline() if not line: break else: break full_output.append(line) print(line.strip()) match = progress_pattern.search(line) if match: current_time = time.time() if current_time - last_update_time >= update_interval: percent = int(match.group(1)) threads = match.group(2) speed = match.group(3) eta = match.group(4) self.progress.emit({ "game": self.game_version, "percent": percent, "threads": threads, "speed": speed, "eta": eta }) last_update_time = current_time return_code = self.process.wait() if not self._is_running: self.finished.emit(False, "下载已手动停止。") return if return_code == 0: self.progress.emit({ "game": self.game_version, "percent": 100, "threads": "N/A", "speed": "N/A", "eta": "完成" }) self.finished.emit(True, "") else: error_message = f"\nAria2c下载失败,退出码: {return_code}\n\n--- Aria2c 输出 ---\n{''.join(full_output)}\n---------------------\n" self.finished.emit(False, error_message) except Exception as e: if self._is_running: self.finished.emit(False, f"\n下载时发生未知错误\n\n【错误信息】: {e}\n") class ProgressWindow(QDialog): def __init__(self, parent=None): super(ProgressWindow, self).__init__(parent) self.setWindowTitle(f"下载进度 - {APP_NAME}") self.resize(450, 180) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowSystemMenuHint) layout = QVBoxLayout() self.game_label = QLabel("正在启动下载,请稍后...") self.progress_bar = QProgressBar() self.progress_bar.setValue(0) self.stats_label = QLabel("速度: - | 线程: - | 剩余时间: -") button_layout = QHBoxLayout() self.pause_resume_button = QtWidgets.QPushButton("暂停下载") self.pause_resume_button.setToolTip("暂停或恢复下载") self.stop_button = QtWidgets.QPushButton("取消下载") self.stop_button.setToolTip("取消整个下载过程") button_layout.addWidget(self.pause_resume_button) button_layout.addWidget(self.stop_button) layout.addWidget(self.game_label) layout.addWidget(self.progress_bar) layout.addWidget(self.stats_label) layout.addLayout(button_layout) self.setLayout(layout) self.is_paused = False self._last_percent = -1 def update_pause_button_state(self, is_paused): """更新暂停按钮的显示状态 Args: is_paused: 是否处于暂停状态 """ self.is_paused = is_paused if is_paused: self.pause_resume_button.setText("恢复下载") else: self.pause_resume_button.setText("暂停下载") def update_progress(self, data): game_version = data.get("game", "未知游戏") percent = data.get("percent", 0) speed = data.get("speed", "-") threads = data.get("threads", "-") eta = data.get("eta", "-") if isinstance(eta, str): eta = eta.replace("]", "") if hasattr(self, '_last_percent') and self._last_percent == percent and percent < 100: self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}") else: self._last_percent = percent self.game_label.setText(f"正在下载 {game_version} 的补丁") self.progress_bar.setValue(int(percent)) self.stats_label.setText(f"速度: {speed} | 线程: {threads} | 剩余时间: {eta}") if percent == 100: self.pause_resume_button.setEnabled(False) self.stop_button.setEnabled(False) self.stop_button.setText("下载完成") QTimer.singleShot(1500, self.accept) def closeEvent(self, event): event.ignore()