From aa4598fbef04534fd7224c04bb70854cfd68f348 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Fri, 9 Jan 2026 22:51:55 +0800 Subject: [PATCH 1/5] feat(IPC): improve shutdown handling for C# IPC client and application lifecycle - Added proper cancellation of asyncio tasks during IPC client shutdown. - Improved logging and handling during application and GUI closure to ensure resources are released cleanly. - Adjusted thread join timeout and refined exception handling in shutdown workflows. --- app/common/IPC_URL/csharp_ipc_handler.py | 38 +++++++++++++++----- app/view/main/window.py | 35 +++++++++++++++--- main.py | 45 +++++++++++++++--------- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 389e1911..28861c71 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 的,所以即使没 join 成功也会随主进程退出 + self.client_thread.join(timeout=0.5) + logger.debug("C# IPC 客户端已停止请求已发出") def send_notification( self, @@ -216,7 +231,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: @@ -229,7 +244,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.Wait()) self.is_connected = True self._disconnect_logged = False @@ -237,10 +252,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..300932fa 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__": From c58327b822776d227e2585957d57bc1427f9d90f Mon Sep 17 00:00:00 2001 From: wsxyt Date: Fri, 9 Jan 2026 22:58:16 +0800 Subject: [PATCH 2/5] docs(changelog): update changelog for v2.2.0 with lifecycle improvements and bug fixes --- CHANGELOG/v2.2.0/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG/v2.2.0/CHANGELOG.md b/CHANGELOG/v2.2.0/CHANGELOG.md index 369318bd..e1c013b9 100644 --- a/CHANGELOG/v2.2.0/CHANGELOG.md +++ b/CHANGELOG/v2.2.0/CHANGELOG.md @@ -8,6 +8,7 @@ v2.0 - Koharu(小鸟游星野) release 3 ## 💡 功能优化 +- 优化 **退出流程**,确保资源释放完整与快速响应 - 优化 **动画流畅性**,新增控件复用减少重绘开销 - 优化 **动画性能**,新增数据缓存减少频繁IO操作 - 优化 **通知渠道选择**,新增ClassIsland使用提示 @@ -21,6 +22,8 @@ v2.0 - Koharu(小鸟游星野) release 3 ## 🐛 修复问题 +- 修复 **程序退出**,解决进程残留与图标未消失问题 +- 修复 **C# IPC 客户端**,解决退出报错与同步异步混合操作 - 修复 **URL命令解析**,修复命令匹配错误 - 修复 **验证窗口线程**,修复线程未清理导致的崩溃 - 修复 **托盘关于功能**,修复绕过安全验证问题 From febab0da693b9730095ecb8a2a320003af0bbb55 Mon Sep 17 00:00:00 2001 From: ws xyt <102407247+WSXYT@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:07:34 +0800 Subject: [PATCH 3/5] Update app/common/IPC_URL/csharp_ipc_handler.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ws xyt <102407247+WSXYT@users.noreply.github.com> --- app/common/IPC_URL/csharp_ipc_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 28861c71..830e9286 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -109,7 +109,7 @@ def stop_ipc_client(self): # 给一点时间让线程退出,但不阻塞太久 # 线程是 daemon 的,所以即使没 join 成功也会随主进程退出 self.client_thread.join(timeout=0.5) - logger.debug("C# IPC 客户端已停止请求已发出") + logger.debug("C# IPC 客户端停止请求已发出") def send_notification( self, From 4a47e2a642856732a299cc168a73d0581eda0177 Mon Sep 17 00:00:00 2001 From: wsxyt Date: Fri, 9 Jan 2026 23:08:11 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(IPC):=20=E8=AE=A9=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E9=80=9A=E8=BF=87=EF=BC=9Aenhance=20task=20h?= =?UTF-8?q?andling=20and=20thread=20management=20in=20C#=20IPC=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed task cancellation to improve reliability during IPC client shutdown. - Resolved a potential issue with task parameter handling in `run_in_executor` calls. - Minor code cleanup for better maintainability. --- app/common/IPC_URL/csharp_ipc_handler.py | 4 ++-- main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 28861c71..e707d6c4 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -104,7 +104,7 @@ def stop_ipc_client(self): 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(): # 给一点时间让线程退出,但不阻塞太久 # 线程是 daemon 的,所以即使没 join 成功也会随主进程退出 @@ -244,7 +244,7 @@ async def client(): self.is_connected = False task = self.ipc_client.Connect() - await self.loop.run_in_executor(None, lambda: task.Wait()) + await self.loop.run_in_executor(None, lambda task=task: task.Wait()) self.is_connected = True self._disconnect_logged = False diff --git a/main.py b/main.py index 300932fa..8f3eb380 100644 --- a/main.py +++ b/main.py @@ -91,7 +91,7 @@ def main(): # 尝试停止所有后台服务 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() From 8bbbd8ffbb80122f392648d01fc21a3fb87e4b62 Mon Sep 17 00:00:00 2001 From: ws xyt <102407247+WSXYT@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:09:03 +0800 Subject: [PATCH 5/5] Update app/common/IPC_URL/csharp_ipc_handler.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ws xyt <102407247+WSXYT@users.noreply.github.com> --- app/common/IPC_URL/csharp_ipc_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/IPC_URL/csharp_ipc_handler.py b/app/common/IPC_URL/csharp_ipc_handler.py index 830e9286..c433edb6 100644 --- a/app/common/IPC_URL/csharp_ipc_handler.py +++ b/app/common/IPC_URL/csharp_ipc_handler.py @@ -107,7 +107,7 @@ def stop_ipc_client(self): if self.client_thread and self.client_thread.is_alive(): # 给一点时间让线程退出,但不阻塞太久 - # 线程是 daemon 的,所以即使没 join 成功也会随主进程退出 + # 线程不是 daemon 的,这里只等待短时间以避免长时间阻塞主线程 self.client_thread.join(timeout=0.5) logger.debug("C# IPC 客户端停止请求已发出")