diff --git a/CHANGELOG/v2.2.0/CHANGELOG.md b/CHANGELOG/v2.2.0/CHANGELOG.md index 7691efde..feef9319 100644 --- a/CHANGELOG/v2.2.0/CHANGELOG.md +++ b/CHANGELOG/v2.2.0/CHANGELOG.md @@ -47,11 +47,13 @@ v2.0 - Koharu(小鸟游星野) release 3 - 修复 **URL命令解析**,修复命令匹配错误 - 修复 **验证窗口线程**,修复线程未清理导致的崩溃 - 修复 **托盘关于功能**,修复绕过安全验证问题 -- 修复 **重启功能**,解决无法重启异常 +- 修复 **重启功能**,彻底解决单一实例导致的重启卡死及命令行窗口弹出问题 - 修复 **URL 注册**,修复注册失败问题 - 修复 **收纳浮窗无焦点**,修复无焦点模式未生效 - 修复 **ClassIsland通知渠道显示时长**,修复时间未生效问题 - 修复 **闪抽颜色设置**,固定颜色未生效 +- 修复 **检查更新时间**,修复从未检查时显示错误及检查成功后时间未更新的问题 +- 修复 **联动设置**,修复设置页面重复打开后显示为空白的问题 ## 🔧 其它变更 diff --git a/app/Language/modules/update.py b/app/Language/modules/update.py index 21f41944..927cb888 100644 --- a/app/Language/modules/update.py +++ b/app/Language/modules/update.py @@ -11,6 +11,7 @@ "check_update": {"name": "检查更新"}, "force_check": {"name": "强制检查更新"}, "check_for_updates": {"name": "检查更新"}, + "never_checked": {"name": "从未检查"}, "checking_update": {"name": "正在检查更新..."}, "update_notification_title": {"name": "SecRandom 更新通知"}, "update_notification_content": {"name": "发现新版本: {version}\n点击查看详情"}, @@ -71,6 +72,7 @@ "latest_version_label": {"name": "Latest version"}, "checking_update": {"name": "Checking for updates..."}, "check_for_updates": {"name": "Check for updates"}, + "never_checked": {"name": "Never checked"}, "already_latest_version": {"name": "You are up to date!"}, "check_update_failed": {"name": "Failed to check for updates"}, "last_check_time": {"name": "Last check update time"}, diff --git a/app/tools/variable.py b/app/tools/variable.py index b5fb1bc0..b718e4f8 100644 --- a/app/tools/variable.py +++ b/app/tools/variable.py @@ -218,6 +218,7 @@ APP_INIT_DELAY = 100 # 应用程序初始化延迟时间(毫秒) FONT_APPLY_DELAY = 0 # 字体应用延迟时间(毫秒) APPLY_DELAY = 0 # 应用延迟时间(毫秒) +EXIT_CODE_RESTART = 1000 # 重启应用程序的退出代码 # -------------------- 设置页面预热配置 -------------------- SETTINGS_WARMUP_INTERVAL_MS = 800 # 后台预热设置页面的默认时间间隔(毫秒) diff --git a/app/view/main/window.py b/app/view/main/window.py index 003ba2ec..629478d6 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -14,7 +14,7 @@ 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, EXIT_CODE_RESTART 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 @@ -816,43 +816,7 @@ def _on_shortcut_settings_changed( def restart_app(self): """重启应用程序(跨平台支持) 执行安全验证后重启程序,清理所有资源""" - try: - working_dir = get_app_root() - - # 过滤掉 --url 等参数 - filtered_args = [arg for arg in sys.argv if not arg.startswith("--")] - - # 获取可执行文件路径 - if getattr(sys, "frozen", False): - # 打包后的可执行文件 - executable = sys.executable - else: - # 开发环境 - executable = sys.executable - - # 跨平台启动新进程 - if sys.platform.startswith("win"): - # Windows 特定参数 - startup_info = subprocess.STARTUPINFO() - startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.Popen( - [executable] + filtered_args, - cwd=working_dir, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS, - startupinfo=startup_info, - ) - else: - # Linux/macOS - subprocess.Popen( - [executable] + filtered_args, - cwd=working_dir, - start_new_session=True, - ) - - except Exception as e: - logger.exception(f"启动新进程失败: {e}") - return + logger.info("正在发起重启请求...") # 停止课前重置定时器 if self.pre_class_reset_timer.isActive(): @@ -861,10 +825,8 @@ def restart_app(self): # 快速清理快捷键 self.cleanup_shortcuts() - # 完全退出当前应用程序 - QApplication.quit() - CSharpIPCHandler.instance().stop_ipc_client() - sys.exit(0) + # 请求重启 + QApplication.exit(EXIT_CODE_RESTART) def _check_pre_class_reset(self): """每秒检测课前重置条件""" diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 335fe416..17ac3e14 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -643,6 +643,10 @@ def _unload_settings_page(self, page_name: str): is_preview=False: settings_window_page.about_page( p, is_preview=is_preview ), + "courseSettingsInterface": lambda p=container, + is_preview=False: settings_window_page.linkage_settings_page( + p, is_preview=is_preview + ), } if page_name in factory_mapping: diff --git a/app/view/settings/update.py b/app/view/settings/update.py index b57b63ba..ccce152e 100644 --- a/app/view/settings/update.py +++ b/app/view/settings/update.py @@ -41,6 +41,8 @@ def run(self): class update(QWidget): """创建更新页面""" + update_check_finished = Signal(bool, str) # 信号:(是否成功, 状态文本) + def __init__(self, parent=None): """初始化更新页面""" super().__init__(parent) @@ -81,6 +83,9 @@ def __init__(self, parent=None): # 连接全局状态管理器的信号 self._connect_global_status_signals() + # 连接内部信号 + self.update_check_finished.connect(self._on_update_check_finished) + def setup_header_info(self): """设置头部信息区域""" # 创建水平布局用于放置状态信息 @@ -297,6 +302,7 @@ def check_for_updates(self, mode="normal"): # 使用异步方式检查更新 def check_update_task(): status_text = "" + is_success = False try: # 获取最新版本信息 latest_version_info = get_latest_version() @@ -318,6 +324,7 @@ def check_update_task(): self.download_install_button.setVisible(True) # 更新全局状态 update_status_manager.set_new_version(latest_version) + is_success = True elif compare_result == 0: # 当前是最新版本 status_text = get_content_name_async( @@ -327,6 +334,7 @@ def check_update_task(): self.download_install_button.setVisible(False) # 更新全局状态 update_status_manager.set_latest_version() + is_success = True else: # 比较失败或版本号异常 status_text = get_content_name_async( @@ -346,39 +354,61 @@ def check_update_task(): # 更新全局状态 update_status_manager.set_check_failed() except Exception as e: + logger.exception(f"检查更新时发生错误: {e}") # 处理异常 status_text = get_content_name_async("update", "check_update_failed") # 隐藏下载并安装按钮 - self.download_install_button.setVisible(False) - # 更新全局状态 - update_status_manager.set_check_failed() - finally: - # 使用QMetaObject.invokeMethod确保UI更新在主线程执行 QMetaObject.invokeMethod( - self, - "_update_check_status", + self.download_install_button, + "setVisible", Qt.QueuedConnection, - Q_ARG(str, status_text), + Q_ARG(bool, False), ) + # 更新全局状态 + update_status_manager.set_check_failed() + finally: + # 发送信号,通知主线程检查完成 + # 信号是线程安全的,可以直接 emit + self.update_check_finished.emit(is_success, status_text) # 创建并启动异步任务 runnable = QRunnable.create(check_update_task) QThreadPool.globalInstance().start(runnable) + @Slot(bool, str) + def _on_update_check_finished(self, is_success: bool, status_text: str): + """处理更新检查完成信号(主线程执行)""" + self.status_label.setText(status_text) + self.indeterminate_ring.setVisible(False) # 隐藏不确定进度环 + self.check_update_button.setEnabled(True) + + if is_success: + logger.debug("收到更新检查成功信号,更新最后检查时间") + self.update_last_check_time() + else: + logger.debug("收到更新检查失败信号") + + @Slot() def _load_last_check_time(self): """加载上次检查更新时间""" last_check_time = readme_settings("update", "last_check_time") + if last_check_time == "1970-01-01 08:00:00" or last_check_time is None: + display_time = get_content_name_async("update", "never_checked") + else: + display_time = last_check_time + self.last_check_label.setText( - f"{get_content_name_async('update', 'last_check_time')}: {last_check_time}" + f"{get_content_name_async('update', 'last_check_time')}: {display_time}" ) - def _update_last_check_time(self): + @Slot() + def update_last_check_time(self): """更新上次检查更新时间为当前时间""" from datetime import datetime current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") update_settings("update", "last_check_time", current_time) - self._load_last_check_time() + QMetaObject.invokeMethod(self, "_load_last_check_time", Qt.QueuedConnection) def download_and_install(self): """下载并安装更新""" @@ -968,9 +998,7 @@ def set_download_cancelled(self): Q_ARG(str, get_content_name_async("update", "update_cancelled")), ) - def update_last_check_time(self): - """更新上次检查时间(公共方法,供外部调用)""" - self._update_last_check_time() + def _restore_from_global_status(self): """从全局状态管理器恢复状态""" diff --git a/main.py b/main.py index a565e947..d8a35299 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import sys import time import gc +import subprocess import sentry_sdk from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels @@ -12,7 +13,7 @@ from app.tools.path_utils import get_app_root from app.tools.config import configure_logging from app.tools.settings_access import readme_settings_async -from app.tools.variable import APP_QUIT_ON_LAST_WINDOW_CLOSED, VERSION +from app.tools.variable import APP_QUIT_ON_LAST_WINDOW_CLOSED, VERSION, EXIT_CODE_RESTART from app.core.single_instance import ( check_single_instance, setup_local_server, @@ -38,8 +39,8 @@ def main(): logger.remove() configure_logging() - # 仅在开发环境(版本号包含 0.0.0)下初始化 Sentry - if VERSION == "v0.0.0": + # 仅在开发环境(版本号不包含 0.0.0)下初始化 Sentry + if "0.0.0" not in VERSION: def before_send(event, hint): # 如果事件中不包含异常信息(即没有堆栈),则不上传 @@ -58,6 +59,7 @@ def before_send(event, hint): before_send=before_send, release=VERSION, send_default_pii=True, + enable_logs=True, ) wm.app_start_time = time.perf_counter() @@ -170,7 +172,7 @@ def new_notify(receiver, event): app.notify = new_notify try: - app.exec() + exit_code = app.exec() logger.debug("Qt 事件循环已结束") # 尝试停止所有后台服务 @@ -202,6 +204,50 @@ def new_notify(receiver, event): logger.info("程序退出流程已完成,正在结束进程") sys.stdout.flush() sys.stderr.flush() + + if exit_code == EXIT_CODE_RESTART: + logger.info("检测到重启信号,正在重启应用程序...") + # 过滤掉 --url 等参数 + filtered_args = [arg for arg in sys.argv if not arg.startswith("--")] + + # 获取可执行文件路径 + if getattr(sys, "frozen", False): + # 打包后的可执行文件 + executable = sys.executable + else: + # 开发环境 + executable = sys.executable + + if not os.path.exists(executable): + logger.critical(f"重启失败:无法找到可执行文件: {executable}") + os._exit(1) + + try: + # 跨平台启动新进程 + if sys.platform.startswith("win"): + # Windows 特定参数 + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + # 使用 CREATE_NO_WINDOW (0x08000000) 来防止创建新的控制台窗口 + CREATE_NO_WINDOW = 0x08000000 + subprocess.Popen( + [executable] + filtered_args, + cwd=program_dir, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP + | CREATE_NO_WINDOW, + startupinfo=startup_info, + ) + else: + # Linux/macOS + subprocess.Popen( + [executable] + filtered_args, + cwd=program_dir, + start_new_session=True, + ) + logger.info("新的应用程序实例已启动") + except Exception as e: + logger.exception(f"重启应用程序失败: {e}") + os._exit(0) except Exception as e: logger.exception(f"程序退出过程中发生异常: {e}")