feat(core): 增强日志记录和错误处理功能

- 更新日志记录机制,将日志文件存储在程序根目录下的log文件夹中,并使用日期+时间戳格式命名。
- 在多个模块中添加详细的错误处理逻辑,确保在发生异常时能够记录相关信息,便于后续排查。
- 优化UI管理器中的日志文件打开功能,增加对日志文件存在性和大小的检查,提升用户体验。
- 在下载管理器和补丁管理器中增强调试信息的记录,确保在关键操作中提供更清晰的反馈。
This commit is contained in:
欧阳淇淇
2025-08-07 00:31:24 +08:00
parent 19cdd5b8cd
commit d12739baab
16 changed files with 614 additions and 225 deletions

View File

@@ -8,15 +8,11 @@ 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
from utils.logger import setup_logger
import signal
import ctypes
import time
from utils.url_censor import censor_url
# 初始化logger
logger = setup_logger("download")
# Windows API常量和函数
if sys.platform == 'win32':
kernel32 = ctypes.windll.kernel32
PROCESS_ALL_ACCESS = 0x1F0FFF
@@ -34,6 +30,7 @@ if sys.platform == 'win32':
('dwFlags', ctypes.c_ulong)
]
# 下载线程类
class DownloadThread(QThread):
progress = Signal(dict)
finished = Signal(bool, str)
@@ -52,10 +49,11 @@ class DownloadThread(QThread):
if self.process and self.process.poll() is None:
self._is_running = False
try:
# 使用 taskkill 强制终止进程及其子进程,并隐藏窗口
subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
logger.error(f"停止下载进程时出错: {e}")
print(f"停止下载进程时出错: {e}")
def _get_process_threads(self, pid):
"""获取进程的所有线程ID"""
if sys.platform != 'win32':
@@ -83,11 +81,13 @@ class DownloadThread(QThread):
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:
logger.warning("未找到可暂停的线程")
print("未找到可暂停的线程")
return False
# 暂停所有线程
for thread_id in self.threads:
h_thread = kernel32.OpenThread(THREAD_SUSPEND_RESUME, False, thread_id)
if h_thread:
@@ -95,15 +95,16 @@ class DownloadThread(QThread):
kernel32.CloseHandle(h_thread)
self._is_paused = True
logger.info(f"下载进程已暂停: PID {self.process.pid}, 线程数: {len(self.threads)}")
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
logger.info(f"下载进程已暂停: PID {self.process.pid}")
print(f"下载进程已暂停: PID {self.process.pid}")
return True
except Exception as e:
logger.error(f"暂停下载进程时出错: {e}")
print(f"暂停下载进程时出错: {e}")
return False
return False
@@ -112,6 +113,7 @@ class DownloadThread(QThread):
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:
@@ -119,22 +121,23 @@ class DownloadThread(QThread):
kernel32.CloseHandle(h_thread)
self._is_paused = False
logger.info(f"下载进程已恢复: PID {self.process.pid}, 线程数: {len(self.threads)}")
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
logger.info(f"下载进程已恢复: PID {self.process.pid}")
print(f"下载进程已恢复: PID {self.process.pid}")
return True
except Exception as e:
logger.error(f"恢复下载进程时出错: {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:
@@ -144,24 +147,29 @@ class DownloadThread(QThread):
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 # 默认值
# 获取主窗口的下载管理器对象
thread_count = 64 # 默认值
if hasattr(self.parent(), 'download_manager'):
# 从下载管理器获取线程数设置
thread_count = self.parent().download_manager.get_download_thread_count()
# 检查是否启用IPv6支持
ipv6_enabled = False
if hasattr(self.parent(), 'config'):
ipv6_enabled = self.parent().config.get("ipv6_enabled", False)
logger.info(f"IPv6支持状态: {ipv6_enabled}")
# 打印IPv6状态
print(f"IPv6支持状态: {ipv6_enabled}")
# 将所有的优化参数应用于每个下载任务
command.extend([
'--dir', download_dir,
'--out', file_name,
@@ -179,7 +187,7 @@ class DownloadThread(QThread):
'--header', 'Sec-Fetch-Site: same-origin',
'--http-accept-gzip=true',
'--console-log-level=notice',
'--summary-interval=1',
'--summary-interval=1',
'--log-level=notice',
'--max-tries=3',
'--retry-wait=2',
@@ -187,40 +195,37 @@ class DownloadThread(QThread):
'--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',
'--split=128',
f'--max-connection-per-server={thread_count}', # 使用动态的线程数
'--min-split-size=1M', # 减小最小分片大小
'--optimize-concurrent-downloads=true', # 优化并发下载
'--file-allocation=none', # 禁用文件预分配加快开始
'--async-dns=true', # 使用异步DNS
])
# 根据IPv6设置决定是否禁用IPv6
if not ipv6_enabled:
command.append('--disable-ipv6=true')
logger.info("已禁用IPv6支持")
print("已禁用IPv6支持")
else:
logger.info("已启用IPv6支持")
print("已启用IPv6支持")
# 证书验证现在总是需要因为我们依赖hosts文件
command.append('--check-certificate=false')
command.append(self.url)
# 创建一个安全的命令副本隐藏URL
safe_command = command.copy()
if len(safe_command) > 0:
# 替换最后一个参数URL为安全版本
url = safe_command[-1]
if isinstance(url, str) and url.startswith("http"):
safe_command[-1] = "***URL protection***"
logger.info(f"即将执行的 Aria2c 命令: {' '.join(safe_command)}")
# 打印将要执行的命令,用于调试
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
# 正则表达式用于解析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\]]+)')
# 添加限流计时器防止更新过于频繁导致UI卡顿
last_update_time = 0
update_interval = 0.2 # 限制UI更新频率每0.2秒最多更新一次
@@ -233,13 +238,12 @@ class DownloadThread(QThread):
else:
break
# 处理输出行隐藏可能包含的URL
censored_line = censor_url(line)
full_output.append(censored_line)
logger.debug(censored_line.strip())
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))
@@ -247,6 +251,7 @@ class DownloadThread(QThread):
speed = match.group(3)
eta = match.group(4)
# 直接发送进度信号不使用invokeMethod
self.progress.emit({
"game": self.game_version,
"percent": percent,
@@ -258,8 +263,9 @@ class DownloadThread(QThread):
last_update_time = current_time
return_code = self.process.wait()
if not self._is_running:
# 如果是手动停止的
self.finished.emit(False, "下载已手动停止。")
return
@@ -280,6 +286,7 @@ class DownloadThread(QThread):
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)
@@ -294,14 +301,18 @@ class ProgressWindow(QDialog):
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)
@@ -311,7 +322,9 @@ class ProgressWindow(QDialog):
layout.addLayout(button_layout)
self.setLayout(layout)
# 设置暂停/恢复状态
self.is_paused = False
# 添加最后进度记录用于优化UI更新
self._last_percent = -1
def update_pause_button_state(self, is_paused):
@@ -333,12 +346,16 @@ class ProgressWindow(QDialog):
threads = data.get("threads", "-")
eta = data.get("eta", "-")
# 清除ETA值中可能存在的"]"符号
if isinstance(eta, str):
eta = eta.replace("]", "")
# 优化UI更新
if hasattr(self, '_last_percent') and self._last_percent == percent and percent < 100:
# 如果百分比没变只更新速度和ETA信息
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))
@@ -351,4 +368,5 @@ class ProgressWindow(QDialog):
QTimer.singleShot(1500, self.accept)
def closeEvent(self, event):
# 覆盖默认的关闭事件,防止用户通过其他方式关闭窗口
event.ignore()