From c26f19b6b318c8f25757bab6f231a8eac0f7dfd8 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 21:37:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(LogManager):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=8C=81=E4=B9=85=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LogManager 单例类,支持日志文件按日期滚动 - 创建 CallerInfoFormatter 自定义格式化器,提取真实调用位置 - 为 MsgBase._showTrace 方法添加日志级别参数,集成日志系统 - 新增 initializeLogManager 初始化函数,日志存储于 AppDataLocation/logs/ - 日志输出格式对齐:[时间] - [类名(15)|级别(8)] - [文件:行号(20:4)] - 消息 - 控制台/INFO级别,全量日志 / DEBUG 级别,错误日志 / ERROR级别 - 全量日志保留7天,错误日志保留14天 --- src/Main.py | 14 ++- src/base/MsgBase.py | 12 ++- src/utils/LogManager.py | 191 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/utils/LogManager.py diff --git a/src/Main.py b/src/Main.py index 47026b1..7fdc459 100644 --- a/src/Main.py +++ b/src/Main.py @@ -16,7 +16,8 @@ from PySide6.QtWidgets import QApplication from gui.ALMainWindow import ALMainWindow from gui.resources import ALResource -from utils.ConfigManager import instance +from utils.ConfigManager import instance as configInstance +from utils.LogManager import instance as logInstance def initializeConfigManager(): @@ -25,7 +26,15 @@ def initializeConfigManager(): config_dir = os.path.join(app_dir, "config") if not QDir(config_dir).exists(): QDir().mkpath(config_dir) - instance(config_dir) + configInstance(config_dir) + +def initializeLogManager(): + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + log_dir = os.path.join(app_dir, "logs") + if not QDir(log_dir).exists(): + QDir().mkpath(log_dir) + logInstance(log_dir) def main(): @@ -36,6 +45,7 @@ def main(): app.setStyle('Fusion') app.setApplicationName("AutoLibrary") initializeConfigManager() + initializeLogManager() window = ALMainWindow() window.show() sys.exit(app.exec_()) diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index bd4f07b..44150ee 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -7,9 +7,12 @@ This software is provided "as is", without any warranty of any kind. You may use, modify, and distribute this file under the terms of the MIT License. See the LICENSE file for details. """ +import logging import queue import datetime +from utils.LogManager import getLogger + class MsgBase: """ @@ -38,6 +41,10 @@ class MsgBase: self._class_name = self.__class__.__name__ self._input_queue = input_queue self._output_queue = output_queue + try: + self._logger = getLogger(self._class_name) + except RuntimeError: + self._logger = None def _showMsg( @@ -50,11 +57,14 @@ class MsgBase: def _showTrace( self, - msg: str + msg: str, + level: int = logging.INFO ): timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}") + if self._logger: + self._logger.log(level, msg) def _waitMsg( diff --git a/src/utils/LogManager.py b/src/utils/LogManager.py new file mode 100644 index 0000000..8af4ae9 --- /dev/null +++ b/src/utils/LogManager.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import logging +import os +import threading + +from logging.handlers import TimedRotatingFileHandler +from typing import Optional + + +class CallerInfoFormatter(logging.Formatter): + """ + Custom formatter to extract real caller information. + Skips MsgBase._showTrace to show the actual calling location. + + Format: + - Logger name: left-aligned, max 15 chars + - Level name: left-aligned, max 8 chars + - Filename: left-aligned, max 20 chars + - Line number: left-aligned, max 4 digits + """ + + def __init__( + self, + fmt=None, + datefmt=None, + style='%' + ): + + super().__init__(fmt, datefmt, style) + self.basefmt = fmt + + def format( + self, + record + ): + + depth = 0 + while depth < 10: + record.filename = os.path.basename(record.pathname) + if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace': + break + if not hasattr(record, 'stack'): + record.stack = True + import traceback + record.stack_list = traceback.extract_stack() + depth += 1 + if depth < len(record.stack_list): + frame = record.stack_list[-depth-1] + record.filename = os.path.basename(frame.filename) + record.lineno = frame.lineno + record.funcName = frame.name + record.name = record.name[-15:].ljust(15) + record.levelname = record.levelname.ljust(8) + record.filename = record.filename[-20:].ljust(20) + record.lineno = f"{record.lineno:04d}" + + return super().format(record) + + +class LogManager: + """ + Log Manager Singleton Class + + Args: + log_dir (str): The directory to store log files. + """ + + def __init__( + self, + log_dir: str + ): + + self.__log_dir = os.path.abspath(log_dir) + self.__logger = None + self.__initialized = False + + self.initialize() + + + def initialize( + self + ): + + if self.__initialized: + return + os.makedirs(self.__log_dir, exist_ok=True) + self.__logger = logging.getLogger("AutoLibrary") + self.__logger.setLevel(logging.DEBUG) + self.__logger.handlers.clear() + + formatter = CallerInfoFormatter( + '[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + self.__logger.addHandler(console_handler) + + all_log_file = os.path.join(self.__log_dir, "all.log") + file_handler_all = TimedRotatingFileHandler( + all_log_file, + when='midnight', + interval=1, + backupCount=7, + encoding='utf-8' + ) + file_handler_all.suffix = "%Y-%m-%d.log" + file_handler_all.setLevel(logging.DEBUG) + file_handler_all.setFormatter(formatter) + self.__logger.addHandler(file_handler_all) + + error_log_file = os.path.join(self.__log_dir, "error.log") + file_handler_error = TimedRotatingFileHandler( + error_log_file, + when='midnight', + interval=1, + backupCount=14, + encoding='utf-8' + ) + file_handler_error.suffix = "%Y-%m-%d.log" + file_handler_error.setLevel(logging.ERROR) + file_handler_error.setFormatter(formatter) + self.__logger.addHandler(file_handler_error) + + self.__initialized = True + + + def getLogger( + self, + name: Optional[str] = None + ) -> logging.Logger: + + if name: + return self.__logger.getChild(name) + return self.__logger + + + def setLevel( + self, + level: int + ): + + if self.__logger: + self.__logger.setLevel(level) + + + def logDir( + self + ) -> str: + + return self.__log_dir + + +# LogManager singleton instance. +_log_manager_instance = None + +# Singleton instance lock. +_instance_lock = threading.Lock() +def instance( + log_dir: str = "" +) -> LogManager: + + global _log_manager_instance + with _instance_lock: + if _log_manager_instance is None: + if not log_dir: + raise ValueError("LogManager initialization requires log_dir parameter") + _log_manager_instance = LogManager(log_dir) + else: + if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir): + raise ValueError("LogManager instance already initialized with a different log directory") + return _log_manager_instance + + +def getLogger( + name: Optional[str] = None +) -> logging.Logger: + + if _log_manager_instance is None: + raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first") + return _log_manager_instance.getLogger(name) From 824b9b886972701878ed82067e7331438db42dc0 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 10:14:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(ALMainWindow):=20=E4=BF=AE=E5=A4=8D=20A?= =?UTF-8?q?LMainWindow=20=E7=9A=84=E9=85=8D=E7=BD=AE=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 先前的实现并未考虑到配置窗口更改时的同步问题,本次提交在 每次配置窗口更改并关闭保存时,同步更新 ALMainWindow 中的配置路径 --- src/gui/ALMainWindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index b0fb9d2..218a518 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -298,6 +298,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed) self.__alConfigWidget.deleteLater() self.__alConfigWidget = None + self.__config_paths = ConfigManager.getValidateAutomationConfigPaths() self.setControlButtons(True, None, None) @Slot(dict) From 2d0782c368c2a0692559947bf32574d49c226589 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 10:17:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(AppInitializer):=20=E5=B0=86?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=80=BB=E8=BE=91=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=88=B0=20AppInitializer=20=E6=A8=A1=E5=9D=97=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本次提交将 Main.py 中的 ConfigManager, LogManager 等初始化逻辑提取到 AppInitializer 模块中 - 更改默认的配置文件路径从 config 目录变为 configs 目录,并考虑兼容性问题 --- src/Main.py | 26 +++---------------- src/utils/AppInitializer.py | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 src/utils/AppInitializer.py diff --git a/src/Main.py b/src/Main.py index 7fdc459..5795315 100644 --- a/src/Main.py +++ b/src/Main.py @@ -10,32 +10,15 @@ See the LICENSE file for details. import os import sys -from PySide6.QtCore import QTranslator, QStandardPaths, QDir +from PySide6.QtCore import QTranslator from PySide6.QtWidgets import QApplication from gui.ALMainWindow import ALMainWindow from gui.resources import ALResource -from utils.ConfigManager import instance as configInstance -from utils.LogManager import instance as logInstance +from utils.AppInitializer import initializeApp -def initializeConfigManager(): - - app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) - config_dir = os.path.join(app_dir, "config") - if not QDir(config_dir).exists(): - QDir().mkpath(config_dir) - configInstance(config_dir) - -def initializeLogManager(): - - app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) - log_dir = os.path.join(app_dir, "logs") - if not QDir(log_dir).exists(): - QDir().mkpath(log_dir) - logInstance(log_dir) - def main(): app = QApplication(sys.argv) @@ -44,13 +27,12 @@ def main(): app.installTranslator(translator) app.setStyle('Fusion') app.setApplicationName("AutoLibrary") - initializeConfigManager() - initializeLogManager() + if not initializeApp(): + sys.exit(-1) window = ALMainWindow() window.show() sys.exit(app.exec_()) - if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/utils/AppInitializer.py b/src/utils/AppInitializer.py new file mode 100644 index 0000000..7e717f0 --- /dev/null +++ b/src/utils/AppInitializer.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 - 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import os + +from PySide6.QtCore import QStandardPaths, QDir + +from utils.ConfigManager import instance as configInstance +from utils.LogManager import instance as logInstance + + +def initializeConfigManager( +) -> bool: + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + old_config_dir = os.path.join(app_dir, "config") + new_config_dir = os.path.join(app_dir, "configs") + if QDir(old_config_dir).exists(): # old config dir exists + #we rename it to compatible with new version + if not QDir().rename(old_config_dir, new_config_dir): + return False + elif not QDir(new_config_dir).exists(): + if not QDir().mkpath(new_config_dir): + return False + configInstance(new_config_dir) + return True + +def initializeLogManager( +) -> bool: + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + log_dir = os.path.join(app_dir, "logs") + if not QDir(log_dir).exists(): + if not QDir().mkpath(log_dir): + return False + logInstance(log_dir) + return True + +def initializeApp( +) -> bool: + + if not initializeConfigManager(): + return False + if not initializeLogManager(): + return False + return True