267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
import os
|
|
import sys
|
|
import psutil
|
|
|
|
from PyQt5.QtGui import QIcon
|
|
from PyQt5.QtWidgets import QSystemTrayIcon, QApplication
|
|
from loguru import logger
|
|
from PyQt5.QtCore import QSharedMemory, QTimer, QObject, pyqtSignal
|
|
import darkdetect
|
|
import datetime as dt
|
|
|
|
from file import base_directory, config_center
|
|
import signal
|
|
|
|
share = QSharedMemory('ClassWidgets')
|
|
_stop_in_progress = False
|
|
|
|
def restart():
|
|
logger.debug('重启程序')
|
|
app = QApplication.instance()
|
|
if app:
|
|
try:
|
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
except (AttributeError, ValueError):
|
|
pass
|
|
app.quit()
|
|
app.processEvents()
|
|
|
|
if share.isAttached():
|
|
share.detach() # 释放共享内存
|
|
os.execl(sys.executable, sys.executable, *sys.argv)
|
|
|
|
def stop(status=0):
|
|
global share, update_timer, _stop_in_progress
|
|
if _stop_in_progress:
|
|
return
|
|
_stop_in_progress = True
|
|
|
|
logger.debug('退出程序...')
|
|
|
|
if 'update_timer' in globals() and update_timer:
|
|
try:
|
|
update_timer.stop()
|
|
update_timer = None
|
|
except Exception as e:
|
|
logger.warning(f"停止全局更新定时器时出错: {e}")
|
|
|
|
app = QApplication.instance()
|
|
if app:
|
|
try:
|
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
except (AttributeError, ValueError):
|
|
pass
|
|
app.quit()
|
|
try:
|
|
current_pid = os.getpid()
|
|
parent = psutil.Process(current_pid)
|
|
children = parent.children(recursive=True)
|
|
if children:
|
|
logger.debug(f"尝试终止 {len(children)} 个子进程...")
|
|
for child in children:
|
|
try:
|
|
logger.debug(f"终止子进程 {child.pid}...")
|
|
child.terminate()
|
|
except psutil.NoSuchProcess:
|
|
logger.debug(f"子进程 {child.pid} 已不存在.")
|
|
continue
|
|
except psutil.AccessDenied:
|
|
logger.warning(f"无权限终止子进程 {child.pid}.")
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(f"终止子进程 {child.pid} 时出错: {e}")
|
|
|
|
gone, alive = psutil.wait_procs(children, timeout=1.5)
|
|
if alive:
|
|
logger.warning(f"{len(alive)} 个子进程未在规定时间内终止,将强制终止...")
|
|
for p in alive:
|
|
try:
|
|
logger.debug(f"强制终止子进程 {p.pid}...")
|
|
p.kill()
|
|
except psutil.NoSuchProcess:
|
|
logger.debug(f"子进程 {p.pid} 在强制终止前已消失.")
|
|
except Exception as e:
|
|
logger.error(f"强制终止子进程 {p.pid} 失败: {e}")
|
|
except psutil.NoSuchProcess:
|
|
logger.warning("无法获取当前进程信息,跳过子进程终止。")
|
|
except Exception as e:
|
|
logger.error(f"终止子进程时出现意外错误: {e}")
|
|
|
|
if 'share' in globals() and share:
|
|
try:
|
|
if share.isAttached():
|
|
share.detach()
|
|
logger.debug("共享内存已分离")
|
|
except Exception as e:
|
|
logger.error(f"分离共享内存时出错: {e}")
|
|
|
|
logger.debug(f"程序退出({status})")
|
|
if not app:
|
|
os._exit(status)
|
|
|
|
def calculate_size(p_w=0.6, p_h=0.7): # 计算尺寸
|
|
screen_geometry = QApplication.primaryScreen().geometry()
|
|
screen_width = screen_geometry.width()
|
|
screen_height = screen_geometry.height()
|
|
|
|
width = int(screen_width * p_w)
|
|
height = int(screen_height * p_h)
|
|
|
|
return (width, height), (int(screen_width / 2 - width / 2), 150)
|
|
|
|
def update_tray_tooltip():
|
|
"""更新托盘文字"""
|
|
if hasattr(sys.modules[__name__], 'tray_icon'):
|
|
tray_instance = getattr(sys.modules[__name__], 'tray_icon')
|
|
if tray_instance is not None:
|
|
schedule_name_from_conf = config_center.read_conf('General', 'schedule')
|
|
if schedule_name_from_conf:
|
|
try:
|
|
schedule_display_name = schedule_name_from_conf
|
|
if schedule_display_name.endswith('.json'):
|
|
schedule_display_name = schedule_display_name[:-5]
|
|
tray_instance.setToolTip(f'Class Widgets - "{schedule_display_name}"')
|
|
logger.info(f'托盘文字更新: "Class Widgets - {schedule_display_name}"')
|
|
except Exception as e:
|
|
logger.error(f"更新托盘提示时发生错误: {e}")
|
|
else:
|
|
tray_instance.setToolTip("Class Widgets - 未加载课表")
|
|
logger.info(f'托盘文字更新: "Class Widgets - 未加载课表"')
|
|
|
|
class DarkModeWatcher(QObject):
|
|
darkModeChanged = pyqtSignal(bool) # 发出暗黑模式变化信号
|
|
def __init__(self, interval=500, parent=None):
|
|
super().__init__(parent)
|
|
self._isDarkMode = darkdetect.isDark() # 初始状态
|
|
self._timer = QTimer(self)
|
|
self._timer.timeout.connect(self._checkTheme)
|
|
self._timer.start(interval) # 轮询间隔(毫秒)
|
|
|
|
def _checkTheme(self):
|
|
currentMode = darkdetect.isDark()
|
|
if currentMode != self._isDarkMode:
|
|
self._isDarkMode = currentMode
|
|
self.darkModeChanged.emit(currentMode) # 发出变化信号
|
|
|
|
def isDark(self):
|
|
"""返回当前是否暗黑模式"""
|
|
return self._isDarkMode
|
|
|
|
def stop(self):
|
|
"""停止监听"""
|
|
self._timer.stop()
|
|
|
|
def start(self, interval=None):
|
|
"""开始监听"""
|
|
if interval:
|
|
self._timer.setInterval(interval)
|
|
self._timer.start()
|
|
|
|
|
|
class TrayIcon(QSystemTrayIcon):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setIcon(QIcon(f"{base_directory}/img/logo/favicon.png"))
|
|
|
|
def push_update_notification(self, text=''):
|
|
self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray
|
|
self.showMessage(
|
|
"发现 Class Widgets 新版本!",
|
|
text,
|
|
QIcon(f"{base_directory}/img/logo/favicon-update.png"),
|
|
5000
|
|
)
|
|
|
|
def push_error_notification(self, title='检查更新失败!', text=''):
|
|
self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray
|
|
self.showMessage(
|
|
title,
|
|
text,
|
|
QIcon(f"{base_directory}/img/logo/favicon-error.ico"),
|
|
5000
|
|
)
|
|
|
|
|
|
class UnionUpdateTimer(QObject):
|
|
"""
|
|
统一更新计时器
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.timer = QTimer(self)
|
|
self.timer.timeout.connect(self._on_timeout)
|
|
self.callbacks = [] # 存储所有的回调函数
|
|
self._is_running = False
|
|
|
|
def _on_timeout(self): # 超时
|
|
app = QApplication.instance()
|
|
if not app or app.closingDown():
|
|
if self.timer.isActive():
|
|
self.timer.stop()
|
|
return
|
|
|
|
# 使用最初的备份列表,防止遍历时修改
|
|
callbacks_copy = self.callbacks[:]
|
|
for callback in callbacks_copy:
|
|
if callback in self.callbacks:
|
|
try:
|
|
callback()
|
|
except RuntimeError as e:
|
|
logger.error(f"回调调用错误 (可能对象已删除): {e}")
|
|
try:
|
|
self.callbacks.remove(callback)
|
|
except ValueError:
|
|
pass
|
|
except Exception as e:
|
|
logger.error(f"执行回调时发生未知错误: {e}")
|
|
if self._is_running:
|
|
self._schedule_next()
|
|
|
|
def _schedule_next(self):
|
|
now = dt.datetime.now()
|
|
next_tick = now.replace(microsecond=0) + dt.timedelta(seconds=1)
|
|
delay = max(0, int((next_tick - now).total_seconds() * 1000))
|
|
self.timer.start(delay)
|
|
|
|
def add_callback(self, callback):
|
|
if callback not in self.callbacks:
|
|
self.callbacks.append(callback)
|
|
if not self._is_running:
|
|
self.start()
|
|
|
|
def remove_callback(self, callback):
|
|
try:
|
|
self.callbacks.remove(callback)
|
|
except ValueError:
|
|
pass
|
|
# if not self.callbacks and self._is_running:
|
|
# self.stop() # 删除定时器
|
|
|
|
def remove_all_callbacks(self):
|
|
self.callbacks = []
|
|
# self.stop() # 删除定时器
|
|
|
|
def start(self):
|
|
if not self._is_running:
|
|
logger.debug("启动 UnionUpdateTimer...")
|
|
self._is_running = True
|
|
self._schedule_next()
|
|
|
|
def stop(self):
|
|
self._is_running = False
|
|
if self.timer:
|
|
try:
|
|
if self.timer.isActive():
|
|
self.timer.stop()
|
|
except RuntimeError as e:
|
|
logger.warning(f"停止 QTimer 时发生运行时错误: {e}")
|
|
except Exception as e:
|
|
logger.error(f"停止 QTimer 时发生未知错误: {e}")
|
|
|
|
|
|
tray_icon = None
|
|
update_timer = UnionUpdateTimer()
|