From 68b6f59dba854ef1dbd0a39f1d11995248581e23 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sat, 3 Jan 2026 23:44:54 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=20IPC=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E5=92=8C=E7=BA=BF=E7=A8=8B=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E8=B5=84=E6=BA=90=E9=87=8A=E6=94=BE?= =?UTF-8?q?=E5=92=8C=E9=80=80=E5=87=BA=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/IPC_URL/csharp_ipc_handler.py | 29 +++++- app/common/safety/secure_store.py | 22 +++-- app/common/shortcut/shortcut_manager.py | 21 ++-- app/tools/update_utils.py | 34 +++++-- app/view/main/window.py | 120 +++++++++++++++++++---- app/view/settings/settings.py | 2 +- 6 files changed, 182 insertions(+), 46 deletions(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 25873237..83afef04 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -81,7 +81,7 @@ def start_ipc_client(self) -> bool: try: self.client_thread = threading.Thread( - target=self._run_client, daemon=False + target=self._run_client, daemon=True ) self.client_thread.start() self.is_running = True @@ -92,9 +92,24 @@ def start_ipc_client(self) -> bool: def stop_ipc_client(self): """停止 C# IPC 客户端""" + logger.debug("正在停止 C# IPC 客户端...") self.is_running = False + + # 尝试主动调用 Dispose 以打破可能挂起的 Connect() 或其他 .NET 调用 + try: + if self.ipc_client and hasattr(self.ipc_client, "Dispose"): + self.ipc_client.Dispose() + logger.debug("已手动调用 C# IPC 客户端 Dispose") + except Exception as e: + logger.debug(f"手动释放 C# IPC 客户端资源时出错: {e}") + if self.client_thread and self.client_thread.is_alive(): - self.client_thread.join(timeout=1) + logger.debug("等待 C# IPC 线程结束...") + # 缩短等待时间,因为线程是 daemon 的,不需要强求完美退出 + self.client_thread.join(timeout=0.2) + if self.client_thread.is_alive(): + logger.debug("C# IPC 线程未能在超时时间内完全结束,将随主进程退出") + logger.debug("C# IPC 客户端停止指令已发出") def send_notification( self, @@ -220,7 +235,7 @@ async def client(): self.is_connected = True while self.is_running: - await asyncio.sleep(1) + await asyncio.sleep(0.1) if not self._check_alive(): if not self._disconnect_logged: @@ -233,6 +248,13 @@ async def client(): self.is_connected = True self._disconnect_logged = False + # 尝试调用 Dispose 释放资源 + try: + if self.ipc_client and hasattr(self.ipc_client, "Dispose"): + self.ipc_client.Dispose() + except Exception as e: + logger.debug(f"释放 C# IPC 客户端时出错: {e}") + self.ipc_client = None self.is_connected = False @@ -291,6 +313,7 @@ def start_ipc_client(self) -> bool: def stop_ipc_client(self): """停止 C# IPC 客户端""" + logger.debug("C# IPC 处理器未启用,无需停止") pass def send_notification( diff --git a/app/common/safety/secure_store.py b/app/common/safety/secure_store.py index 96ed9259..6a2ece23 100644 --- a/app/common/safety/secure_store.py +++ b/app/common/safety/secure_store.py @@ -102,10 +102,13 @@ def read_secrets() -> dict: # 不存在则创建空文件 if not os.path.exists(p): ensure_dir(os.path.dirname(p)) - with open(p, "wb") as f: - f.write(b"") - _set_hidden(str(p)) - logger.debug(f"创建空安全配置文件:{p}") + try: + with open(p, "w", encoding="utf-8") as f: + json.dump({}, f) + _set_hidden(str(p)) + logger.debug(f"创建空安全配置文件:{p}") + except Exception as e: + logger.warning(f"创建安全配置文件失败: {e}") return {} if os.path.exists(p): try: @@ -185,10 +188,13 @@ def read_behind_scenes_settings() -> dict: p = get_settings_path("behind_scenes.json") if not os.path.exists(p): ensure_dir(os.path.dirname(p)) - with open(p, "wb") as f: - f.write(b"") - _set_hidden(str(p)) - logger.debug(f"创建空内幕设置文件:{p}") + try: + with open(p, "w", encoding="utf-8") as f: + json.dump({}, f) + _set_hidden(str(p)) + logger.debug(f"创建空内幕设置文件:{p}") + except Exception as e: + logger.warning(f"创建内幕设置文件失败: {e}") return {} if os.path.exists(p): try: diff --git a/app/common/shortcut/shortcut_manager.py b/app/common/shortcut/shortcut_manager.py index f8f4fc5c..d193f2d0 100644 --- a/app/common/shortcut/shortcut_manager.py +++ b/app/common/shortcut/shortcut_manager.py @@ -221,10 +221,17 @@ def is_enabled(self) -> bool: def cleanup(self): """清理所有快捷键""" - logger.info("清理所有快捷键") - for config_key, hotkey in self.shortcuts.items(): - try: - keyboard.remove_hotkey(hotkey) - except Exception as e: - logger.error(f"注销快捷键失败 {config_key}: {e}") - self.shortcuts.clear() + if self.shortcuts: + logger.info(f"清理已注册的 {len(self.shortcuts)} 个快捷键") + for config_key, hotkey in self.shortcuts.items(): + try: + keyboard.remove_hotkey(hotkey) + except Exception as e: + logger.error(f"注销快捷键失败 {config_key}: {e}") + self.shortcuts.clear() + + # 无论是否有已注册快捷键,都尝试清理全局钩子,确保 keyboard 库的监听线程能正确关闭 + try: + keyboard.unhook_all() + except Exception: + pass diff --git a/app/tools/update_utils.py b/app/tools/update_utils.py index be0d891f..95928daa 100644 --- a/app/tools/update_utils.py +++ b/app/tools/update_utils.py @@ -588,13 +588,13 @@ async def get_latest_version_async(channel: int | None = None) -> dict | None: latest_no = metadata.get("latest_no", {}) # 获取版本信息,如果通道不存在则使用稳定通道的版本 - version = latest.get(channel_name, latest.get("release", VERSION)) + version = latest.get(channel_name, latest.get("release", SPECIAL_VERSION)) version_no = latest_no.get(channel_name, latest_no.get("release", 0)) # 如果版本号是Disable,返回当前版本,禁止该通道更新 if version == "Disable": logger.debug(f"通道 {channel_name} 已禁用,返回当前版本") - return {"version": VERSION, "version_no": 0} + return {"version": SPECIAL_VERSION, "version_no": 0} logger.debug( f"获取最新版本信息成功: 通道={channel_name}, 版本={version}, 版本号={version_no}" @@ -628,7 +628,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: latest_version (str): 最新版本号,格式为 "vX.X.X"、"vX.X.X.X" 或 "vX.X.X-alpha.1" 等 Returns: - int: 1 表示有新版本,0 表示版本相同,-1 表示比较失败 + int: 1 表示有新版本,0 表示版本相同,-1 表示当前版本更新,-2 表示比较失败 """ try: # 检查版本号是否为空 @@ -636,7 +636,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: logger.error( f"比较版本号失败: 版本号为空,current={current_version}, latest={latest_version}" ) - return -1 + return -2 # 移除版本号前缀 "v" current = current_version.lstrip("v") @@ -693,7 +693,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: return 0 # 版本号完全相同 except Exception as e: logger.error(f"比较版本号失败: {e}") - return -1 + return -2 def get_update_download_url( @@ -1582,7 +1582,7 @@ def safe_call_update_interface(method_name, *args): latest_version_no = latest_version_info["version_no"] # 比较版本号 - compare_result = compare_versions(VERSION, latest_version) + compare_result = compare_versions(SPECIAL_VERSION, latest_version) # 获取下载文件夹路径 download_dir = get_data_path("downloads") @@ -1687,6 +1687,10 @@ def format_size(size_bytes): str(expected_file_path), file_size_str, ) + # 更新全局状态 + update_status_manager.set_download_complete_with_size( + str(expected_file_path), file_size_str + ) return else: # 文件损坏,需要重新下载 @@ -1781,16 +1785,24 @@ def format_size(size_bytes): safe_call_update_interface("set_download_failed") # 更新全局状态 update_status_manager.set_download_failed() - elif compare_result == 0: - # 当前是最新版本 - logger.debug("当前已是最新版本") + elif compare_result == 0 or compare_result == -1: + # 当前是最新版本或开发版本 + if compare_result == 0: + logger.debug("当前已是最新版本") + else: + logger.debug(f"当前版本 ({SPECIAL_VERSION}) 比远程版本 ({latest_version}) 更新,视为最新版本") + # 通知更新页面已是最新版本 safe_call_update_interface("set_latest_version") + # 更新全局状态 + update_status_manager.set_latest_version() else: - # 版本比较失败 + # 版本比较失败 (compare_result == -2) logger.debug("版本比较失败") # 通知更新页面检查失败 safe_call_update_interface("set_check_failed") + # 更新全局状态 + update_status_manager.set_check_failed() # 更新上次检查时间 safe_call_update_interface("update_last_check_time") @@ -1798,6 +1810,8 @@ def format_size(size_bytes): logger.error(f"启动时检查更新失败: {e}") # 通知更新页面检查失败 safe_call_update_interface("set_check_failed") + # 更新全局状态 + update_status_manager.set_check_failed() finally: # 关闭事件循环 if loop and not loop.is_closed(): diff --git a/app/view/main/window.py b/app/view/main/window.py index 6e216b30..87f43532 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -2,18 +2,19 @@ # 导入库 # ================================================== import sys +import os import subprocess import loguru from loguru import logger from PySide6.QtWidgets import QApplication, QWidget from PySide6.QtGui import QIcon -from PySide6.QtCore import QTimer, QEvent, Signal +from PySide6.QtCore import QTimer, QEvent, Signal, QSharedMemory from qfluentwidgets import FluentWindow, NavigationItemPosition from app.common.IPC_URL.csharp_ipc_handler import CSharpIPCHandler from app.common.shortcut import ShortcutManager -from app.tools.variable import MINIMUM_WINDOW_SIZE, APP_INIT_DELAY +from app.tools.variable import MINIMUM_WINDOW_SIZE, APP_INIT_DELAY, SHARED_MEMORY_KEY from app.tools.path_utils import get_data_path, get_app_root from app.tools.personalised import get_theme_icon from app.tools.settings_access import get_safe_font_size @@ -32,6 +33,7 @@ from app.view.tray.tray import Tray from app.view.floating_window.levitation import LevitationWindow from app.common.IPC_URL.url_command_handler import URLCommandHandler +from app.common.music.music_player import music_player # ================================================== @@ -624,23 +626,77 @@ def toggle_window(self): def close_window_secrandom(self): """关闭窗口 执行安全验证后关闭程序,释放所有资源""" + logger.info("开始执行程序退出流程...") + + # 尝试停止更新检查线程 + try: + from app.tools import update_utils + if hasattr(update_utils, "update_check_thread") and update_utils.update_check_thread: + if update_utils.update_check_thread.isRunning(): + logger.debug("正在停止更新检查线程...") + update_utils.update_check_thread.terminate() + update_utils.update_check_thread.wait(500) + except Exception as e: + logger.debug(f"停止更新检查线程时出错: {e}") + + # 停止所有后台服务(音乐、语音等) + self._stop_all_services() + # 停止课前重置定时器 - if self.pre_class_reset_timer.isActive(): + if hasattr(self, "pre_class_reset_timer") and self.pre_class_reset_timer.isActive(): + logger.debug("停止课前重置定时器") self.pre_class_reset_timer.stop() self.cleanup_shortcuts() + + CSharpIPCHandler.instance().stop_ipc_client() + + logger.info("正在请求退出 QApplication...") + QApplication.quit() + + # 尝试处理最后残留的事件 + QApplication.processEvents() + + # 显式释放共享内存 try: - loguru.logger.remove() + shared_mem = QSharedMemory(SHARED_MEMORY_KEY) + if shared_mem.attach(): + shared_mem.detach() + logger.debug("已分离共享内存") except Exception as e: - logger.error(f"日志系统关闭出错: {e}") + logger.debug(f"释放共享内存时出错: {e}") - QApplication.quit() - CSharpIPCHandler.instance().stop_ipc_client() - sys.exit(0) + logger.info("退出流程执行完毕,终止进程") + # 刷新标准流并强制退出,解决退出挂起及 Python 解释器清理时的崩溃问题 + try: + sys.stderr.flush() + sys.stdout.flush() + except Exception: + pass + os._exit(0) + + def _stop_all_services(self): + """停止所有后台服务(音乐、语音等)""" + try: + # 停止全局音乐播放器 + if music_player: + music_player.stop_music(fade_out=False) + except Exception as e: + logger.debug(f"停止音乐播放器时出错: {e}") + + try: + # 停止各页面的语音播放器 + for page in [self.roll_call_page, self.lottery_page]: + if page and hasattr(page, "tts_handler") and page.tts_handler: + page.tts_handler.stop() + except Exception as e: + logger.debug(f"停止语音处理器时出错: {e}") def cleanup_shortcuts(self): """清理快捷键""" if hasattr(self, "shortcut_manager"): + if self.shortcut_manager.shortcuts: + logger.debug("正在清理所有快捷键...") self.shortcut_manager.cleanup() def _connect_shortcut_signals(self): @@ -755,9 +811,8 @@ def _on_shortcut_settings_changed( self, first_level_key: str, second_level_key: str, value ): """当快捷键设置发生变化时的处理函数""" - logger.debug(f"快捷键设置变化: {first_level_key}.{second_level_key} = {value}") - if first_level_key == "shortcut_settings": + logger.debug(f"快捷键设置变化: {first_level_key}.{second_level_key} = {value}") if second_level_key == "enable_shortcut": logger.debug(f"快捷键启用状态变化: {value}") self.shortcut_manager.set_enabled(value) @@ -789,21 +844,52 @@ def restart_app(self): logger.error(f"启动新进程失败: {e}") return + # 尝试停止更新检查线程 + try: + from app.tools import update_utils + if hasattr(update_utils, "update_check_thread") and update_utils.update_check_thread: + if update_utils.update_check_thread.isRunning(): + logger.debug("正在停止更新检查线程...") + update_utils.update_check_thread.terminate() + update_utils.update_check_thread.wait(500) + except Exception as e: + logger.debug(f"停止更新检查线程时出错: {e}") + + # 停止所有后台服务(音乐、语音等) + self._stop_all_services() + # 停止课前重置定时器 - if self.pre_class_reset_timer.isActive(): + if hasattr(self, "pre_class_reset_timer") and self.pre_class_reset_timer.isActive(): + logger.debug("停止课前重置定时器") self.pre_class_reset_timer.stop() self.cleanup_shortcuts() + CSharpIPCHandler.instance().stop_ipc_client() + + logger.info("正在请求退出 QApplication 以进行重启...") + QApplication.quit() + + # 尝试处理最后残留的事件 + QApplication.processEvents() + + # 显式释放共享内存 try: - loguru.logger.remove() + shared_mem = QSharedMemory(SHARED_MEMORY_KEY) + if shared_mem.attach(): + shared_mem.detach() + logger.debug("已分离共享内存") except Exception as e: - logger.error(f"日志系统关闭出错: {e}") + logger.debug(f"释放共享内存时出错: {e}") - # 完全退出当前应用程序 - QApplication.quit() - CSharpIPCHandler.instance().stop_ipc_client() - sys.exit(0) + logger.info("重启前的清理流程执行完毕,终止当前进程") + # 刷新标准流并强制退出,解决退出挂起及 Python 解释器清理时的崩溃问题 + try: + sys.stderr.flush() + sys.stdout.flush() + except Exception: + pass + os._exit(0) def _check_pre_class_reset(self): """每秒检测课前重置条件""" diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 58e07e14..020d415b 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -69,7 +69,7 @@ def __init__(self, parent=None, is_preview=False): self.show() # 初始化子界面 - QTimer.singleShot(APP_INIT_DELAY, lambda: (self.createSubInterface())) + self.createSubInterface() @property def is_preview(self): From 521345ae5aac5573103d002c2747234674044844 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 00:46:58 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=B8=8E=E8=B5=84=E6=BA=90=E9=87=8A=E6=94=BE?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=20IPC=20=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E9=80=80=E5=87=BA=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/IPC_URL/csharp_ipc_handler.py | 122 +++++++++++-------- app/core/window_manager.py | 13 ++- app/tools/config.py | 6 +- app/tools/update_utils.py | 39 ++++++- app/view/main/window.py | 142 ++++++++++++----------- app/view/settings/settings.py | 2 +- main.py | 1 + 7 files changed, 199 insertions(+), 126 deletions(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 83afef04..e269a663 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -66,6 +66,7 @@ def __init__(self): self.client_thread: Optional[threading.Thread] = None self.is_running = False self.is_connected = False + self._stop_event = threading.Event() self._disconnect_logged = False # 跟踪是否已记录断连日志 self._last_on_class_left_log_time = 0 # 上次记录距离上课时间的时间 @@ -80,8 +81,9 @@ def start_ipc_client(self) -> bool: return True try: + self._stop_event.clear() self.client_thread = threading.Thread( - target=self._run_client, daemon=True + target=self._run_client, daemon=False ) self.client_thread.start() self.is_running = True @@ -92,23 +94,23 @@ def start_ipc_client(self) -> bool: def stop_ipc_client(self): """停止 C# IPC 客户端""" + if not self.is_running: + return + logger.debug("正在停止 C# IPC 客户端...") self.is_running = False - - # 尝试主动调用 Dispose 以打破可能挂起的 Connect() 或其他 .NET 调用 - try: - if self.ipc_client and hasattr(self.ipc_client, "Dispose"): - self.ipc_client.Dispose() - logger.debug("已手动调用 C# IPC 客户端 Dispose") - except Exception as e: - logger.debug(f"手动释放 C# IPC 客户端资源时出错: {e}") + self._stop_event.set() if self.client_thread and self.client_thread.is_alive(): logger.debug("等待 C# IPC 线程结束...") - # 缩短等待时间,因为线程是 daemon 的,不需要强求完美退出 - self.client_thread.join(timeout=0.2) + # 给予足够的时间正常退出,因为 daemon=False + self.client_thread.join(timeout=1.0) if self.client_thread.is_alive(): - logger.debug("C# IPC 线程未能在超时时间内完全结束,将随主进程退出") + logger.warning("C# IPC 线程未能在超时时间内完全结束,可能由于 .NET 调用阻塞") + + # 线程结束后再清理资源,避免竞态条件 + self.ipc_client = None + self.is_connected = False logger.debug("C# IPC 客户端停止指令已发出") def send_notification( @@ -223,46 +225,74 @@ def _run_client(self): async def client(): """异步客户端""" - - self.ipc_client = IpcClient() - self.ipc_client.JsonIpcProvider.AddNotifyHandler( - IpcRoutedNotifyIds.OnClassNotifyId, - Action(lambda: self._on_class_test()), - ) - - task = self.ipc_client.Connect() - await loop.run_in_executor(None, lambda: task.Wait()) - self.is_connected = True - - while self.is_running: - await asyncio.sleep(0.1) - - if not self._check_alive(): - if not self._disconnect_logged: - logger.debug("C# IPC 断连!重连...") - self._disconnect_logged = True - self.is_connected = False - - task = self.ipc_client.Connect() - await loop.run_in_executor(None, task.Wait) - self.is_connected = True - self._disconnect_logged = False - - # 尝试调用 Dispose 释放资源 try: - if self.ipc_client and hasattr(self.ipc_client, "Dispose"): - self.ipc_client.Dispose() + self.ipc_client = IpcClient() + self.ipc_client.JsonIpcProvider.AddNotifyHandler( + IpcRoutedNotifyIds.OnClassNotifyId, + Action(lambda: self._on_class_test()), + ) + + task = self.ipc_client.Connect() + # 优化:在等待连接时定期检查 is_running 标志,以便快速响应退出请求 + # 避免在 .NET 内部 Wait() 导致线程无法被 Python 正常终止 + while self.is_running and not task.IsCompleted: + await asyncio.sleep(0.1) + + if not self.is_running: + return + + if task.IsFaulted: + logger.error(f"C# IPC 连接失败: {task.Exception}") + self.is_connected = False + return + + self.is_connected = True + + while self.is_running: + # 使用 wait 替代 sleep,提高响应速度并降低 CPU 占用 + await asyncio.sleep(0.5) + + if not self.is_running: + break + + if not self._check_alive(): + if not self._disconnect_logged: + logger.debug("C# IPC 断连!重连...") + self._disconnect_logged = True + self.is_connected = False + + task = self.ipc_client.Connect() + while self.is_running and not task.IsCompleted: + await asyncio.sleep(0.1) + + if not self.is_running: + break + + if task.IsFaulted: + continue + + self.is_connected = True + self._disconnect_logged = False except Exception as e: - logger.debug(f"释放 C# IPC 客户端时出错: {e}") - - self.ipc_client = None - self.is_connected = False + if self.is_running: + logger.error(f"C# IPC 客户端运行出错: {e}") + finally: + # 在线程内部安全地释放资源 + try: + if self.ipc_client and hasattr(self.ipc_client, "Dispose"): + self.ipc_client.Dispose() + logger.debug("C# IPC 客户端资源已释放 (Dispose)") + except Exception as e: + logger.debug(f"释放 C# IPC 客户端资源时出错: {e}") + self.is_connected = False # 启动新的 asyncio 事件循环 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - loop.run_until_complete(client()) - loop.close() + try: + loop.run_until_complete(client()) + finally: + loop.close() def _check_alive(self) -> bool: """客户端是否正常连接""" diff --git a/app/core/window_manager.py b/app/core/window_manager.py index c8e9a583..fb19a23e 100644 --- a/app/core/window_manager.py +++ b/app/core/window_manager.py @@ -19,6 +19,15 @@ def __init__(self) -> None: self.settings_window: Optional["QWidget"] = None self.float_window: Optional["QWidget"] = None self.url_handler: Optional = None + self.shared_memory = None + + def set_shared_memory(self, shared_memory) -> None: + """设置共享内存实例 + + Args: + shared_memory: QSharedMemory 实例 + """ + self.shared_memory = shared_memory def set_url_handler(self, url_handler) -> None: """设置URL处理器 @@ -42,7 +51,9 @@ def _create_main_window_impl(self) -> None: self.create_float_window() self.main_window = MainWindow( - float_window=self.float_window, url_handler_instance=self.url_handler + float_window=self.float_window, + url_handler_instance=self.url_handler, + shared_memory=self.shared_memory, ) self._connect_main_window_signals() diff --git a/app/tools/config.py b/app/tools/config.py index a034eb8d..9c91ee3f 100644 --- a/app/tools/config.py +++ b/app/tools/config.py @@ -1301,7 +1301,7 @@ def _apply_export_all_warning(): file_path, _ = QFileDialog.getSaveFileName( parent, get_content_pushbutton_name_async("basic_settings", "export_all_data"), - f"SecRandom_{SPECIAL_VERSION}_all_data.zip", + f"SecRandom_{VERSION}_all_data.zip", "ZIP Files (*.zip);;All Files (*)", ) if not file_path: @@ -1320,7 +1320,7 @@ def _apply_export_all_warning(): with zipfile.ZipFile(file_path, "w", zipfile.ZIP_DEFLATED) as zipf: version_info = { "software_name": "SecRandom", - "version": SPECIAL_VERSION, + "version": VERSION, } zipf.writestr( "version.json", json.dumps(version_info, ensure_ascii=False, indent=2) @@ -1403,7 +1403,7 @@ def import_all_data(parent: Optional[QWidget] = None) -> None: if version_info: software_name = version_info.get("software_name", "") version = version_info.get("version", "") - current_version = SPECIAL_VERSION + current_version = VERSION if software_name != "SecRandom" or version != current_version: _mismatch_cancelled = False diff --git a/app/tools/update_utils.py b/app/tools/update_utils.py index 95928daa..9bc5568d 100644 --- a/app/tools/update_utils.py +++ b/app/tools/update_utils.py @@ -588,13 +588,13 @@ async def get_latest_version_async(channel: int | None = None) -> dict | None: latest_no = metadata.get("latest_no", {}) # 获取版本信息,如果通道不存在则使用稳定通道的版本 - version = latest.get(channel_name, latest.get("release", SPECIAL_VERSION)) + version = latest.get(channel_name, latest.get("release", VERSION)) version_no = latest_no.get(channel_name, latest_no.get("release", 0)) # 如果版本号是Disable,返回当前版本,禁止该通道更新 if version == "Disable": logger.debug(f"通道 {channel_name} 已禁用,返回当前版本") - return {"version": SPECIAL_VERSION, "version_no": 0} + return {"version": VERSION, "version_no": 0} logger.debug( f"获取最新版本信息成功: 通道={channel_name}, 版本={version}, 版本号={version_no}" @@ -628,7 +628,11 @@ def compare_versions(current_version: str, latest_version: str) -> int: latest_version (str): 最新版本号,格式为 "vX.X.X"、"vX.X.X.X" 或 "vX.X.X-alpha.1" 等 Returns: - int: 1 表示有新版本,0 表示版本相同,-1 表示当前版本更新,-2 表示比较失败 + int: 比较结果代码: + 1 - 远程版本较新(建议更新) + 0 - 版本相同 + -1 - 当前版本较新(处于开发或预览分支) + -2 - 比较失败(版本号格式错误或为空) """ try: # 检查版本号是否为空 @@ -1527,6 +1531,11 @@ class UpdateCheckThread(QThread): def __init__(self, settings_window=None): super().__init__() self.settings_window = settings_window + self._is_running = True + + def stop(self): + """请求停止线程""" + self._is_running = False def run(self): """执行更新检查""" @@ -1543,6 +1552,8 @@ def run(self): # 辅助函数:安全地调用更新页面的方法 def safe_call_update_interface(method_name, *args): """安全地调用更新页面的方法""" + if not self._is_running: + return if self.settings_window and hasattr( self.settings_window, "updateInterface" ): @@ -1561,6 +1572,8 @@ def safe_call_update_interface(method_name, *args): logger.debug("自动更新模式为0,不执行更新检查") return + if not self._is_running: return + # 通知更新页面开始检查 safe_call_update_interface("set_checking_status") # 更新全局状态 @@ -1570,6 +1583,8 @@ def safe_call_update_interface(method_name, *args): logger.debug("开始检查更新") latest_version_info = loop.run_until_complete(get_latest_version_async()) + if not self._is_running: return + if not latest_version_info: logger.debug("获取最新版本信息失败") # 通知更新页面检查失败 @@ -1582,7 +1597,7 @@ def safe_call_update_interface(method_name, *args): latest_version_no = latest_version_info["version_no"] # 比较版本号 - compare_result = compare_versions(SPECIAL_VERSION, latest_version) + compare_result = compare_versions(VERSION, latest_version) # 获取下载文件夹路径 download_dir = get_data_path("downloads") @@ -1790,7 +1805,7 @@ def format_size(size_bytes): if compare_result == 0: logger.debug("当前已是最新版本") else: - logger.debug(f"当前版本 ({SPECIAL_VERSION}) 比远程版本 ({latest_version}) 更新,视为最新版本") + logger.debug(f"当前版本 ({VERSION}) 比远程版本 ({latest_version}) 更新,视为最新版本") # 通知更新页面已是最新版本 safe_call_update_interface("set_latest_version") @@ -1832,3 +1847,17 @@ def check_for_updates_on_startup(settings_window=None): update_check_thread = UpdateCheckThread(settings_window) update_check_thread.start() return update_check_thread + + +def stop_update_check(): + """停止更新检查线程""" + global update_check_thread + if update_check_thread and update_check_thread.isRunning(): + logger.debug("停止更新检查线程...") + update_check_thread.stop() + # 给予一定时间正常退出 + if not update_check_thread.wait(1000): + logger.warning("更新检查线程未能在超时时间内正常退出,强制终止") + update_check_thread.terminate() + update_check_thread.wait(500) + update_check_thread = None diff --git a/app/view/main/window.py b/app/view/main/window.py index 87f43532..ddd550f8 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -50,13 +50,20 @@ class MainWindow(FluentWindow): showTrayActionRequested = Signal(str) # 请求执行托盘操作 classIslandDataReceived = Signal(dict) # 接收ClassIsland数据信号 - def __init__(self, float_window: LevitationWindow, url_handler_instance=None): + def __init__( + self, + float_window: LevitationWindow, + url_handler_instance=None, + shared_memory=None, + ): super().__init__() # 设置窗口对象名称,方便其他组件查找 self.setObjectName("MainWindow") # 保存URL处理器实例引用 self.url_handler_instance = url_handler_instance + # 保存共享内存引用 + self.shared_memory_instance = shared_memory self.roll_call_page = None self.settingsInterface = None @@ -628,75 +635,106 @@ def close_window_secrandom(self): 执行安全验证后关闭程序,释放所有资源""" logger.info("开始执行程序退出流程...") - # 尝试停止更新检查线程 + self._perform_cleanup() + + logger.info("正在请求退出 QApplication...") + QApplication.quit() + + # 尝试处理最后残留的事件 + QApplication.processEvents() + + logger.info("退出流程执行完毕,终止进程") + sys.exit(0) + + def _perform_cleanup(self): + """执行通用的清理逻辑,确保程序能够干净地退出""" + # 1. 停止更新检查线程 try: from app.tools import update_utils - if hasattr(update_utils, "update_check_thread") and update_utils.update_check_thread: - if update_utils.update_check_thread.isRunning(): - logger.debug("正在停止更新检查线程...") - update_utils.update_check_thread.terminate() - update_utils.update_check_thread.wait(500) + # 使用标准的 API 停止线程,避免使用危险的 terminate() + if hasattr(update_utils, "stop_update_check"): + update_utils.stop_update_check() + elif hasattr(update_utils, "update_check_thread") and update_utils.update_check_thread: + thread = update_utils.update_check_thread + if thread.isRunning(): + logger.debug("正在请求停止更新检查线程...") + if hasattr(thread, "stop"): + thread.stop() + else: + thread.terminate() + thread.wait(1000) except Exception as e: - logger.debug(f"停止更新检查线程时出错: {e}") + logger.debug(f"停止更新检查线程时发生非致命错误: {e}") - # 停止所有后台服务(音乐、语音等) + # 2. 停止所有后台服务(音乐、语音等) self._stop_all_services() - # 停止课前重置定时器 + # 3. 停止课前重置定时器 if hasattr(self, "pre_class_reset_timer") and self.pre_class_reset_timer.isActive(): logger.debug("停止课前重置定时器") self.pre_class_reset_timer.stop() + # 4. 清理快捷键 self.cleanup_shortcuts() + # 5. 停止 C# IPC 客户端 + # 注意:stop_ipc_client 内部现在已经处理了优雅退出逻辑 CSharpIPCHandler.instance().stop_ipc_client() - logger.info("正在请求退出 QApplication...") - QApplication.quit() - - # 尝试处理最后残留的事件 - QApplication.processEvents() - - # 显式释放共享内存 + # 6. 显式释放共享内存 try: - shared_mem = QSharedMemory(SHARED_MEMORY_KEY) - if shared_mem.attach(): - shared_mem.detach() - logger.debug("已分离共享内存") + # 优先使用保存的共享内存实例进行分离 + if self.shared_memory_instance: + if self.shared_memory_instance.isAttached(): + self.shared_memory_instance.detach() + logger.debug("已通过主实例分离共享内存") + else: + # 备选方案:尝试创建临时实例并分离(虽然不推荐,但可作为最后尝试) + shared_mem = QSharedMemory(SHARED_MEMORY_KEY) + if shared_mem.attach(): + shared_mem.detach() + logger.debug("已通过临时实例分离共享内存") except Exception as e: - logger.debug(f"释放共享内存时出错: {e}") + logger.debug(f"释放共享内存时发生非致命错误: {e}") - logger.info("退出流程执行完毕,终止进程") - # 刷新标准流并强制退出,解决退出挂起及 Python 解释器清理时的崩溃问题 + # 7. 刷新标准流,确保所有日志都已写入 try: sys.stderr.flush() sys.stdout.flush() except Exception: + # 忽略流刷新错误,因为流可能已经关闭 pass - os._exit(0) def _stop_all_services(self): """停止所有后台服务(音乐、语音等)""" try: # 停止全局音乐播放器 - if music_player: + from app.common.music.music_player import music_player + # 只有在正在播放时才尝试停止 + if music_player.is_playing(): music_player.stop_music(fade_out=False) except Exception as e: - logger.debug(f"停止音乐播放器时出错: {e}") + logger.debug(f"停止音乐播放器时发生非致命错误: {e}") try: # 停止各页面的语音播放器 - for page in [self.roll_call_page, self.lottery_page]: - if page and hasattr(page, "tts_handler") and page.tts_handler: + # 显式检查页面是否存在,避免 AttributeError + pages_to_cleanup = [] + for attr_name in ["roll_call_page", "lottery_page"]: + page_obj = getattr(self, attr_name, None) + if page_obj is not None: + pages_to_cleanup.append(page_obj) + + for page in pages_to_cleanup: + if hasattr(page, "tts_handler") and page.tts_handler: page.tts_handler.stop() except Exception as e: - logger.debug(f"停止语音处理器时出错: {e}") + logger.debug(f"停止语音处理器时发生非致命错误: {e}") def cleanup_shortcuts(self): """清理快捷键""" if hasattr(self, "shortcut_manager"): - if self.shortcut_manager.shortcuts: - logger.debug("正在清理所有快捷键...") + # 内部 cleanup 会判断是否有快捷键需要清理,并处理日志 self.shortcut_manager.cleanup() def _connect_shortcut_signals(self): @@ -844,28 +882,7 @@ def restart_app(self): logger.error(f"启动新进程失败: {e}") return - # 尝试停止更新检查线程 - try: - from app.tools import update_utils - if hasattr(update_utils, "update_check_thread") and update_utils.update_check_thread: - if update_utils.update_check_thread.isRunning(): - logger.debug("正在停止更新检查线程...") - update_utils.update_check_thread.terminate() - update_utils.update_check_thread.wait(500) - except Exception as e: - logger.debug(f"停止更新检查线程时出错: {e}") - - # 停止所有后台服务(音乐、语音等) - self._stop_all_services() - - # 停止课前重置定时器 - if hasattr(self, "pre_class_reset_timer") and self.pre_class_reset_timer.isActive(): - logger.debug("停止课前重置定时器") - self.pre_class_reset_timer.stop() - - self.cleanup_shortcuts() - - CSharpIPCHandler.instance().stop_ipc_client() + self._perform_cleanup() logger.info("正在请求退出 QApplication 以进行重启...") QApplication.quit() @@ -873,23 +890,8 @@ def restart_app(self): # 尝试处理最后残留的事件 QApplication.processEvents() - # 显式释放共享内存 - try: - shared_mem = QSharedMemory(SHARED_MEMORY_KEY) - if shared_mem.attach(): - shared_mem.detach() - logger.debug("已分离共享内存") - except Exception as e: - logger.debug(f"释放共享内存时出错: {e}") - logger.info("重启前的清理流程执行完毕,终止当前进程") - # 刷新标准流并强制退出,解决退出挂起及 Python 解释器清理时的崩溃问题 - try: - sys.stderr.flush() - sys.stdout.flush() - except Exception: - pass - os._exit(0) + sys.exit(0) def _check_pre_class_reset(self): """每秒检测课前重置条件""" diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 020d415b..58e07e14 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -69,7 +69,7 @@ def __init__(self, parent=None, is_preview=False): self.show() # 初始化子界面 - self.createSubInterface() + QTimer.singleShot(APP_INIT_DELAY, lambda: (self.createSubInterface())) @property def is_preview(self): diff --git a/main.py b/main.py index 71df545f..d8e7247c 100644 --- a/main.py +++ b/main.py @@ -68,6 +68,7 @@ def main(): app.setAttribute(Qt.ApplicationAttribute.AA_DontCreateNativeWidgetSiblings) window_manager = WindowManager() + window_manager.set_shared_memory(shared_memory) url_handler = create_url_handler() cs_ipc_handler = create_cs_ipc_handler() window_manager.set_url_handler(url_handler) From 070148086d6f5d544a8fa892cc50d37d37952389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=8E=E6=B3=BD=E6=87=BF=5FAionflux?= <139693537+lzy98276@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:52:12 +0800 Subject: [PATCH 3/9] Update app/common/shortcut/shortcut_manager.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: 黎泽懿_Aionflux <139693537+lzy98276@users.noreply.github.com> --- app/common/shortcut/shortcut_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/common/shortcut/shortcut_manager.py b/app/common/shortcut/shortcut_manager.py index d193f2d0..db768489 100644 --- a/app/common/shortcut/shortcut_manager.py +++ b/app/common/shortcut/shortcut_manager.py @@ -233,5 +233,5 @@ def cleanup(self): # 无论是否有已注册快捷键,都尝试清理全局钩子,确保 keyboard 库的监听线程能正确关闭 try: keyboard.unhook_all() - except Exception: - pass + except Exception as e: + logger.warning(f"清理全局键盘钩子失败: {e}") From 794664763fdf5e3df99fcbae851bd4d7c2dd856a Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 01:02:09 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E6=94=B9=E7=94=A8=20SPECIAL=5FVERSION=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20VERSION=20=E5=8F=98=E9=87=8F=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=AF=BC=E5=87=BA=E4=B8=8E=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tools/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/tools/config.py b/app/tools/config.py index 9c91ee3f..a034eb8d 100644 --- a/app/tools/config.py +++ b/app/tools/config.py @@ -1301,7 +1301,7 @@ def _apply_export_all_warning(): file_path, _ = QFileDialog.getSaveFileName( parent, get_content_pushbutton_name_async("basic_settings", "export_all_data"), - f"SecRandom_{VERSION}_all_data.zip", + f"SecRandom_{SPECIAL_VERSION}_all_data.zip", "ZIP Files (*.zip);;All Files (*)", ) if not file_path: @@ -1320,7 +1320,7 @@ def _apply_export_all_warning(): with zipfile.ZipFile(file_path, "w", zipfile.ZIP_DEFLATED) as zipf: version_info = { "software_name": "SecRandom", - "version": VERSION, + "version": SPECIAL_VERSION, } zipf.writestr( "version.json", json.dumps(version_info, ensure_ascii=False, indent=2) @@ -1403,7 +1403,7 @@ def import_all_data(parent: Optional[QWidget] = None) -> None: if version_info: software_name = version_info.get("software_name", "") version = version_info.get("version", "") - current_version = VERSION + current_version = SPECIAL_VERSION if software_name != "SecRandom" or version != current_version: _mismatch_cancelled = False From f0df4d8b9c9abf620cec153bca3aed6fdba1d703 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 12:54:11 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=20IPC=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E9=80=80=E5=87=BA=E6=B5=81=E7=A8=8B=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20SPECIAL=5FVERSION=20=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20VERSION=20=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/IPC_URL/csharp_ipc_handler.py | 7 +++++-- app/tools/update_utils.py | 2 +- app/view/settings/update.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index e269a663..5b035f83 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -249,8 +249,11 @@ async def client(): self.is_connected = True while self.is_running: - # 使用 wait 替代 sleep,提高响应速度并降低 CPU 占用 - await asyncio.sleep(0.5) + # 缩短轮询间隔,提高退出响应速度 (每次睡眠 0.1s,共 0.5s) + for _ in range(5): + if not self.is_running: + break + await asyncio.sleep(0.1) if not self.is_running: break diff --git a/app/tools/update_utils.py b/app/tools/update_utils.py index 9bc5568d..ad63d22b 100644 --- a/app/tools/update_utils.py +++ b/app/tools/update_utils.py @@ -1597,7 +1597,7 @@ def safe_call_update_interface(method_name, *args): latest_version_no = latest_version_info["version_no"] # 比较版本号 - compare_result = compare_versions(VERSION, latest_version) + compare_result = compare_versions(SPECIAL_VERSION, latest_version) # 获取下载文件夹路径 download_dir = get_data_path("downloads") diff --git a/app/view/settings/update.py b/app/view/settings/update.py index 2af529da..92e88321 100644 --- a/app/view/settings/update.py +++ b/app/view/settings/update.py @@ -309,7 +309,7 @@ def check_update_task(): if mode == "force": compare_result = compare_versions("v0.0.0", latest_version) else: - compare_result = compare_versions(VERSION, latest_version) + compare_result = compare_versions(SPECIAL_VERSION, latest_version) if compare_result == 1: # 有新版本 From 4b96c6b22b9cbf4756a7c9a81dbea8d2f347f950 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 12:57:29 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E6=94=B9=E7=94=A8=20VERSION=20=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=20SPECIAL=5FVERSION=20=E5=8F=98=E9=87=8F=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=89=88=E6=9C=AC=E6=A0=A1=E9=AA=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/view/settings/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/view/settings/update.py b/app/view/settings/update.py index 92e88321..2af529da 100644 --- a/app/view/settings/update.py +++ b/app/view/settings/update.py @@ -309,7 +309,7 @@ def check_update_task(): if mode == "force": compare_result = compare_versions("v0.0.0", latest_version) else: - compare_result = compare_versions(SPECIAL_VERSION, latest_version) + compare_result = compare_versions(VERSION, latest_version) if compare_result == 1: # 有新版本 From 7c5bfc3fba482a56370107f8c4c41e90d2397892 Mon Sep 17 00:00:00 2001 From: ws xyt <102407247+WSXYT@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:58:22 +0800 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ws xyt <102407247+WSXYT@users.noreply.github.com> --- app/view/main/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/view/main/window.py b/app/view/main/window.py index ddd550f8..1571c490 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -702,8 +702,9 @@ def _perform_cleanup(self): sys.stderr.flush() sys.stdout.flush() except Exception: - # 忽略流刷新错误,因为流可能已经关闭 - pass + except Exception as e: + # 在退出前刷新标准流失败时忽略异常,但记录调试信息 + logger.debug(f"刷新标准输出/错误流时出错: {e}") def _stop_all_services(self): """停止所有后台服务(音乐、语音等)""" From 720f16adc5bb0884832390e4a190e582e3d011f4 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 13:18:55 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=AF=94=E8=BE=83=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=A4=9A=E4=BD=99=E7=BA=BF=E7=A8=8B=E7=8A=B6=E6=80=81=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E4=B8=8E=E5=85=A8=E5=B1=80=E7=8A=B6=E6=80=81=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tools/update_utils.py | 45 +++++++-------------------------------- app/view/main/window.py | 1 - 2 files changed, 8 insertions(+), 38 deletions(-) diff --git a/app/tools/update_utils.py b/app/tools/update_utils.py index ad63d22b..cb102c59 100644 --- a/app/tools/update_utils.py +++ b/app/tools/update_utils.py @@ -628,11 +628,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: latest_version (str): 最新版本号,格式为 "vX.X.X"、"vX.X.X.X" 或 "vX.X.X-alpha.1" 等 Returns: - int: 比较结果代码: - 1 - 远程版本较新(建议更新) - 0 - 版本相同 - -1 - 当前版本较新(处于开发或预览分支) - -2 - 比较失败(版本号格式错误或为空) + int: 1 表示有新版本,0 表示版本相同,-1 表示比较失败 """ try: # 检查版本号是否为空 @@ -640,7 +636,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: logger.error( f"比较版本号失败: 版本号为空,current={current_version}, latest={latest_version}" ) - return -2 + return -1 # 移除版本号前缀 "v" current = current_version.lstrip("v") @@ -697,7 +693,7 @@ def compare_versions(current_version: str, latest_version: str) -> int: return 0 # 版本号完全相同 except Exception as e: logger.error(f"比较版本号失败: {e}") - return -2 + return -1 def get_update_download_url( @@ -1531,11 +1527,6 @@ class UpdateCheckThread(QThread): def __init__(self, settings_window=None): super().__init__() self.settings_window = settings_window - self._is_running = True - - def stop(self): - """请求停止线程""" - self._is_running = False def run(self): """执行更新检查""" @@ -1552,8 +1543,6 @@ def run(self): # 辅助函数:安全地调用更新页面的方法 def safe_call_update_interface(method_name, *args): """安全地调用更新页面的方法""" - if not self._is_running: - return if self.settings_window and hasattr( self.settings_window, "updateInterface" ): @@ -1572,8 +1561,6 @@ def safe_call_update_interface(method_name, *args): logger.debug("自动更新模式为0,不执行更新检查") return - if not self._is_running: return - # 通知更新页面开始检查 safe_call_update_interface("set_checking_status") # 更新全局状态 @@ -1583,8 +1570,6 @@ def safe_call_update_interface(method_name, *args): logger.debug("开始检查更新") latest_version_info = loop.run_until_complete(get_latest_version_async()) - if not self._is_running: return - if not latest_version_info: logger.debug("获取最新版本信息失败") # 通知更新页面检查失败 @@ -1597,7 +1582,7 @@ def safe_call_update_interface(method_name, *args): latest_version_no = latest_version_info["version_no"] # 比较版本号 - compare_result = compare_versions(SPECIAL_VERSION, latest_version) + compare_result = compare_versions(VERSION, latest_version) # 获取下载文件夹路径 download_dir = get_data_path("downloads") @@ -1702,10 +1687,6 @@ def format_size(size_bytes): str(expected_file_path), file_size_str, ) - # 更新全局状态 - update_status_manager.set_download_complete_with_size( - str(expected_file_path), file_size_str - ) return else: # 文件损坏,需要重新下载 @@ -1800,24 +1781,16 @@ def format_size(size_bytes): safe_call_update_interface("set_download_failed") # 更新全局状态 update_status_manager.set_download_failed() - elif compare_result == 0 or compare_result == -1: - # 当前是最新版本或开发版本 - if compare_result == 0: - logger.debug("当前已是最新版本") - else: - logger.debug(f"当前版本 ({VERSION}) 比远程版本 ({latest_version}) 更新,视为最新版本") - + elif compare_result == 0: + # 当前是最新版本 + logger.debug("当前已是最新版本") # 通知更新页面已是最新版本 safe_call_update_interface("set_latest_version") - # 更新全局状态 - update_status_manager.set_latest_version() else: - # 版本比较失败 (compare_result == -2) + # 版本比较失败 logger.debug("版本比较失败") # 通知更新页面检查失败 safe_call_update_interface("set_check_failed") - # 更新全局状态 - update_status_manager.set_check_failed() # 更新上次检查时间 safe_call_update_interface("update_last_check_time") @@ -1825,8 +1798,6 @@ def format_size(size_bytes): logger.error(f"启动时检查更新失败: {e}") # 通知更新页面检查失败 safe_call_update_interface("set_check_failed") - # 更新全局状态 - update_status_manager.set_check_failed() finally: # 关闭事件循环 if loop and not loop.is_closed(): diff --git a/app/view/main/window.py b/app/view/main/window.py index 1571c490..35b4f9cc 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -701,7 +701,6 @@ def _perform_cleanup(self): try: sys.stderr.flush() sys.stdout.flush() - except Exception: except Exception as e: # 在退出前刷新标准流失败时忽略异常,但记录调试信息 logger.debug(f"刷新标准输出/错误流时出错: {e}") From 9818315920afdac45bac1e87af51e88130916251 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Sun, 4 Jan 2026 13:43:46 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E7=BA=BF=E7=A8=8B=E7=BB=88=E6=AD=A2=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=94=B9=E7=94=A8=20terminate=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=BC=BA=E5=88=B6=E7=BB=88=E6=AD=A2=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E9=98=BB=E5=A1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tools/update_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tools/update_utils.py b/app/tools/update_utils.py index cb102c59..cd710855 100644 --- a/app/tools/update_utils.py +++ b/app/tools/update_utils.py @@ -1825,7 +1825,7 @@ def stop_update_check(): global update_check_thread if update_check_thread and update_check_thread.isRunning(): logger.debug("停止更新检查线程...") - update_check_thread.stop() + update_check_thread.terminate() # 直接强制终止 # 给予一定时间正常退出 if not update_check_thread.wait(1000): logger.warning("更新检查线程未能在超时时间内正常退出,强制终止")