Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG/v2.2.0/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ v2.0 - Koharu(小鸟游星野) release 3
- 修复 **URL命令解析**,修复命令匹配错误
- 修复 **验证窗口线程**,修复线程未清理导致的崩溃
- 修复 **托盘关于功能**,修复绕过安全验证问题
- 修复 **重启功能**,解决无法重启异常
- 修复 **重启功能**,彻底解决单一实例导致的重启卡死及命令行窗口弹出问题
- 修复 **URL 注册**,修复注册失败问题
- 修复 **收纳浮窗无焦点**,修复无焦点模式未生效
- 修复 **ClassIsland通知渠道显示时长**,修复时间未生效问题
- 修复 **闪抽颜色设置**,固定颜色未生效
- 修复 **检查更新时间**,修复从未检查时显示错误及检查成功后时间未更新的问题
- 修复 **联动设置**,修复设置页面重复打开后显示为空白的问题

## 🔧 其它变更

Expand Down
2 changes: 2 additions & 0 deletions app/Language/modules/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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点击查看详情"},
Expand Down Expand Up @@ -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"},
Expand Down
1 change: 1 addition & 0 deletions app/tools/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
APP_INIT_DELAY = 100 # 应用程序初始化延迟时间(毫秒)
FONT_APPLY_DELAY = 0 # 字体应用延迟时间(毫秒)
APPLY_DELAY = 0 # 应用延迟时间(毫秒)
EXIT_CODE_RESTART = 1000 # 重启应用程序的退出代码

# -------------------- 设置页面预热配置 --------------------
SETTINGS_WARMUP_INTERVAL_MS = 800 # 后台预热设置页面的默认时间间隔(毫秒)
Expand Down
46 changes: 4 additions & 42 deletions app/view/main/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# 导入库
# ==================================================
import os
import sys

Check failure on line 5 in app/view/main/window.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

app/view/main/window.py:5:8: F401 `sys` imported but unused
import shutil
import subprocess

Check failure on line 7 in app/view/main/window.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

app/view/main/window.py:7:8: F401 `subprocess` imported but unused

from loguru import logger
from PySide6.QtWidgets import QApplication, QWidget
Expand All @@ -14,8 +14,8 @@

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

Check failure on line 18 in app/view/main/window.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

app/view/main/window.py:18:49: F401 `app.tools.path_utils.get_app_root` imported but unused
from app.tools.personalised import get_theme_icon
from app.tools.settings_access import get_safe_font_size
from app.Language.obtain_language import (
Expand Down Expand Up @@ -816,43 +816,7 @@
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():
Expand All @@ -861,10 +825,8 @@
# 快速清理快捷键
self.cleanup_shortcuts()

# 完全退出当前应用程序
QApplication.quit()
CSharpIPCHandler.instance().stop_ipc_client()
sys.exit(0)
# 请求重启
QApplication.exit(EXIT_CODE_RESTART)

def _check_pre_class_reset(self):
"""每秒检测课前重置条件"""
Expand Down
4 changes: 4 additions & 0 deletions app/view/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 42 additions & 14 deletions app/view/settings/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def run(self):
class update(QWidget):
"""创建更新页面"""

update_check_finished = Signal(bool, str) # 信号:(是否成功, 状态文本)

def __init__(self, parent=None):
"""初始化更新页面"""
super().__init__(parent)
Expand Down Expand Up @@ -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):
"""设置头部信息区域"""
# 创建水平布局用于放置状态信息
Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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):
"""下载并安装更新"""
Expand Down Expand Up @@ -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):
"""从全局状态管理器恢复状态"""
Expand Down
54 changes: 50 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import time
import gc
import subprocess

import sentry_sdk
from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels
Expand All @@ -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,
Expand All @@ -38,8 +39,8 @@ def main():
logger.remove()
configure_logging()

# 仅在开发环境(版本号包含 0.0.0)下初始化 Sentry
if VERSION == "v0.0.0":
# 仅在开发环境(版本号不包含 0.0.0)下初始化 Sentry
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incorrect - it states "仅在开发环境(版本号不包含 0.0.0)下初始化 Sentry" (Only initialize Sentry in development environment where version doesn't contain 0.0.0), but the condition if "0.0.0" not in VERSION: actually initializes Sentry in non-development builds (when version doesn't contain "0.0.0"). The comment should say "仅在非开发环境" (Only in non-development environment) or the logic should be inverted to match the comment.

Suggested change
# 仅在开发环境(版本号不包含 0.0.0)下初始化 Sentry
# 仅在非开发环境(版本号不包含 0.0.0)下初始化 Sentry

Copilot uses AI. Check for mistakes.
if "0.0.0" not in VERSION:

def before_send(event, hint):
# 如果事件中不包含异常信息(即没有堆栈),则不上传
Expand All @@ -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()
Expand Down Expand Up @@ -170,7 +172,7 @@ def new_notify(receiver, event):
app.notify = new_notify

try:
app.exec()
exit_code = app.exec()
logger.debug("Qt 事件循环已结束")

# 尝试停止所有后台服务
Expand Down Expand Up @@ -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("--")]
Comment on lines +210 to +211
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The restart logic filters out command-line arguments starting with "--" on line 211, but then unconditionally includes filtered_args[1:] implicitly. The first element of sys.argv (sys.argv[0]) is the script/executable name, and should be excluded from filtered_args. Currently, if sys.argv[0] doesn't start with "--", it will be included in filtered_args and passed as an argument to the new process. The filter should exclude sys.argv[0] by using filtered_args = [arg for arg in sys.argv[1:] if not arg.startswith("--")].

Suggested change
# 过滤掉 --url 等参数
filtered_args = [arg for arg in sys.argv if not arg.startswith("--")]
# 过滤掉 --url 等参数,并且跳过 sys.argv[0](可执行文件路径)
filtered_args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]

Copilot uses AI. Check for mistakes.

# 获取可执行文件路径
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}")
Expand Down
Loading