diff --git a/CHANGELOG/v2.2.0/CHANGELOG.md b/CHANGELOG/v2.2.0/CHANGELOG.md index 359a7cb1..b037671b 100644 --- a/CHANGELOG/v2.2.0/CHANGELOG.md +++ b/CHANGELOG/v2.2.0/CHANGELOG.md @@ -10,6 +10,8 @@ v2.0 - Koharu(小鸟游星野) release 3 ## 💡 功能优化 +- 优化 **退出流程**,确保资源释放完整与快速响应 +- 优化 **动画流畅性**,新增控件复用减少重绘开销 - 优化 **闪抽动画日志**,减少不必要的日志输出 - 优化 **动画性能**,新增数据缓存减少频繁IO操作 - 优化 **通知渠道选择**,新增ClassIsland使用提示 @@ -24,6 +26,8 @@ v2.0 - Koharu(小鸟游星野) release 3 ## 🐛 修复问题 +- 修复 **程序退出**,解决进程残留与图标未消失问题 +- 修复 **C# IPC 客户端**,解决退出报错与同步异步混合操作 - 修复 **URL命令解析**,修复命令匹配错误 - 修复 **验证窗口线程**,修复线程未清理导致的崩溃 - 修复 **托盘关于功能**,修复绕过安全验证问题 diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 943676ce..03837102 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -64,6 +64,7 @@ def __init__(self): """ self.ipc_client: Optional[IpcClient] = None self.client_thread: Optional[threading.Thread] = None + self.loop: Optional[asyncio.AbstractEventLoop] = None self.is_running = False self.is_connected = False self._disconnect_logged = False # 跟踪是否已记录断连日志 @@ -80,21 +81,35 @@ def start_ipc_client(self) -> bool: return True try: + self.is_running = True self.client_thread = threading.Thread( target=self._run_client, daemon=False ) self.client_thread.start() - self.is_running = True return True except Exception as e: + self.is_running = False logger.error(f"启动 C# IPC 客户端失败: {e}") return False def stop_ipc_client(self): """停止 C# IPC 客户端""" + logger.debug("正在停止 C# IPC 客户端...") self.is_running = False + if self.loop and self.loop.is_running(): + # 获取所有正在运行的任务并取消它们 + # 这会使 await asyncio.sleep(1) 等操作抛出 CancelledError + try: + for task in asyncio.all_tasks(self.loop): + self.loop.call_soon_threadsafe(task.cancel) + except Exception as e: + logger.warning(f"取消 IPC 客户端任务时出错: {e}") + if self.client_thread and self.client_thread.is_alive(): - self.client_thread.join(timeout=1) + # 给一点时间让线程退出,但不阻塞太久 + # 线程不是 daemon 的,这里只等待短时间以避免长时间阻塞主线程 + self.client_thread.join(timeout=0.5) + logger.debug("C# IPC 客户端停止请求已发出") def send_notification( self, @@ -220,7 +235,7 @@ async def client(): ) task = self.ipc_client.Connect() - await loop.run_in_executor(None, lambda: task.Wait()) + await self.loop.run_in_executor(None, lambda: task.Wait()) self.is_connected = True while self.is_running: @@ -233,7 +248,7 @@ async def client(): self.is_connected = False task = self.ipc_client.Connect() - await loop.run_in_executor(None, task.Wait) + await self.loop.run_in_executor(None, lambda task=task: task.Wait()) self.is_connected = True self._disconnect_logged = False @@ -241,10 +256,17 @@ async def client(): self.is_connected = False # 启动新的 asyncio 事件循环 - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(client()) - loop.close() + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + try: + self.loop.run_until_complete(client()) + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"C# IPC 客户端循环出错: {e}") + finally: + self.loop.close() + self.loop = None def _check_alive(self) -> bool: """客户端是否正常连接""" diff --git a/app/view/main/window.py b/app/view/main/window.py index b5d43d62..df274699 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -625,17 +625,44 @@ def toggle_window(self): def close_window_secrandom(self): """关闭窗口 执行安全验证后关闭程序,释放所有资源""" + logger.info("程序正在退出 (close_window_secrandom)...") + # 停止课前重置定时器 - if self.pre_class_reset_timer.isActive(): + if hasattr(self, "pre_class_reset_timer") and self.pre_class_reset_timer.isActive(): self.pre_class_reset_timer.stop() + logger.debug("课前重置定时器已停止") # 快速清理快捷键 self.cleanup_shortcuts() + logger.debug("快捷键已清理") + + # 停止 IPC 客户端 + try: + CSharpIPCHandler.instance().stop_ipc_client() + logger.debug("C# IPC 停止请求已发出") + except Exception as e: + logger.error(f"停止 IPC 客户端失败: {e}") - # 直接退出,不等待日志清理 + # 显式关闭所有顶层窗口(包括悬浮窗、设置窗口等) + try: + top_level_widgets = QApplication.topLevelWidgets() + logger.debug(f"正在关闭所有顶层窗口,共 {len(top_level_widgets)} 个") + for widget in top_level_widgets: + if widget != self: + logger.debug(f"正在关闭窗口: {widget.objectName() or widget}") + widget.close() + if hasattr(widget, "hide"): + widget.hide() + except Exception as e: + logger.error(f"关闭其他窗口时出错: {e}") + + # 最后关闭自己 + logger.debug("正在关闭主窗口...") + self.close() + + # 请求退出应用程序 + logger.info("已发出 QApplication.quit() 请求") QApplication.quit() - CSharpIPCHandler.instance().stop_ipc_client() - sys.exit(0) def cleanup_shortcuts(self): """清理快捷键""" diff --git a/main.py b/main.py index 71df545f..8f3eb380 100644 --- a/main.py +++ b/main.py @@ -86,36 +86,47 @@ def main(): try: app.exec() + logger.debug("Qt 事件循环已结束") + + # 尝试停止所有后台服务 + if 'cs_ipc_handler' in locals() and cs_ipc_handler: + cs_ipc_handler.stop_ipc_client() + + if 'url_handler' in locals() and url_handler: + if hasattr(url_handler, 'url_ipc_handler'): + url_handler.url_ipc_handler.stop_ipc_server() shared_memory.detach() + logger.debug("共享内存已释放") if local_server: local_server.close() + logger.debug("本地服务器已关闭") if update_check_thread and update_check_thread.isRunning(): - logger.debug("等待更新检查线程完成...") - update_check_thread.wait(5000) + logger.debug("正在等待更新检查线程完成...") + update_check_thread.wait(2000) if update_check_thread.isRunning(): - logger.warning("更新检查线程未在超时时间内完成,强制终止") + logger.warning("更新检查线程超时,强行退出") + else: + logger.debug("更新检查线程已安全完成") gc.collect() + logger.debug("垃圾回收已完成") - sys.exit() + logger.info("程序退出流程已完成,正在结束进程") + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) except Exception as e: - logger.error(f"应用程序启动失败: {e}") - shared_memory.detach() - - if local_server: + logger.error(f"程序退出过程中发生异常: {e}") + if 'shared_memory' in locals(): + shared_memory.detach() + if 'local_server' in locals() and local_server: local_server.close() - - try: - if update_check_thread and update_check_thread.isRunning(): - logger.debug("等待更新检查线程完成...") - update_check_thread.wait(5000) - except Exception as thread_e: - logger.exception("处理更新检查线程时发生错误: {}", thread_e) - - sys.exit(1) + sys.stdout.flush() + sys.stderr.flush() + os._exit(1) if __name__ == "__main__":