Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG/v2.2.0/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ v2.0 - Koharu(小鸟游星野) release 3

## 💡 功能优化

- 优化 **退出流程**,确保资源释放完整与快速响应
- 优化 **动画流畅性**,新增控件复用减少重绘开销
- 优化 **闪抽动画日志**,减少不必要的日志输出
- 优化 **动画性能**,新增数据缓存减少频繁IO操作
- 优化 **通知渠道选择**,新增ClassIsland使用提示
Expand All @@ -24,6 +26,8 @@ v2.0 - Koharu(小鸟游星野) release 3

## 🐛 修复问题

- 修复 **程序退出**,解决进程残留与图标未消失问题
- 修复 **C# IPC 客户端**,解决退出报错与同步异步混合操作
- 修复 **URL命令解析**,修复命令匹配错误
- 修复 **验证窗口线程**,修复线程未清理导致的崩溃
- 修复 **托盘关于功能**,修复绕过安全验证问题
Expand Down
38 changes: 30 additions & 8 deletions app/common/IPC_URL/csharp_ipc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 # 跟踪是否已记录断连日志
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -233,18 +248,25 @@ 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

self.ipc_client = None
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:
"""客户端是否正常连接"""
Expand Down
35 changes: 31 additions & 4 deletions app/view/main/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""清理快捷键"""
Expand Down
45 changes: 28 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
Loading