From 8f0f80c6447b6e2d6edd0d47e07532fd31d8f83e Mon Sep 17 00:00:00 2001 From: leimingsheng Date: Wed, 27 Aug 2025 09:02:49 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E6=96=B0=E5=A2=9E=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=9F=A5=E9=98=85=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resource/MainWindow.ui | 47 ++++++++++++++++++++++++++++++ src/MainWindow_ui.py | 25 ++++++++++++++++ src/ZiJin_mergeLog.py | 58 +++++++++++++++++++++++++++++++++++++ src/background_task.py | 25 +++++++++++++++- src/event_handler.py | 27 +++++++++++++++++- src/main_window.py | 13 ++++++++- src/search_shortcut.py | 65 ++++++++++++++++++++++++++++++++++++++++++ src/service.py | 18 +++++++++++- src/ui_initializer.py | 31 +++++++++++++++++--- src/utils.py | 53 +++++++++++++++++++++++++++++++++- 10 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 src/ZiJin_mergeLog.py create mode 100644 src/search_shortcut.py diff --git a/resource/MainWindow.ui b/resource/MainWindow.ui index b85551d..81df7b2 100644 --- a/resource/MainWindow.ui +++ b/resource/MainWindow.ui @@ -151,6 +151,53 @@ p, li { white-space: pre-wrap; } 事件时间线 + + + 日志查看 + + + + + + IDL日志 + + + + IDL日志 + + + + + 审计日志 + + + + + 维护日志 + + + + + OS串口日志 + + + + + 运行状态 + + + + + 部件日志 + + + + + + + + + diff --git a/src/MainWindow_ui.py b/src/MainWindow_ui.py index e994bc2..ba33ffc 100644 --- a/src/MainWindow_ui.py +++ b/src/MainWindow_ui.py @@ -76,6 +76,23 @@ class Ui_MainWindow(object): self.tab_timeline_event = QtWidgets.QWidget() self.tab_timeline_event.setObjectName("tab_timeline_event") self.tabWidget.addTab(self.tab_timeline_event, "") + self.tab_readlog = QtWidgets.QWidget() + self.tab_readlog.setObjectName("tab_readlog") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab_readlog) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.comboBox_logSelect = QtWidgets.QComboBox(self.tab_readlog) + self.comboBox_logSelect.setObjectName("comboBox_logSelect") + self.comboBox_logSelect.addItem("") + self.comboBox_logSelect.addItem("") + self.comboBox_logSelect.addItem("") + self.comboBox_logSelect.addItem("") + self.comboBox_logSelect.addItem("") + self.comboBox_logSelect.addItem("") + self.verticalLayout_2.addWidget(self.comboBox_logSelect) + self.textBrowser_logView = QtWidgets.QTextBrowser(self.tab_readlog) + self.textBrowser_logView.setObjectName("textBrowser_logView") + self.verticalLayout_2.addWidget(self.textBrowser_logView) + self.tabWidget.addTab(self.tab_readlog, "") self.horizontalLayout_2.addWidget(self.tabWidget) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) @@ -120,6 +137,14 @@ class Ui_MainWindow(object): self.comboBox.setItemText(10, _translate("MainWindow", "VR_FPGA_Temp")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_temp_all), _translate("MainWindow", "温度信息")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_timeline_event), _translate("MainWindow", "事件时间线")) + self.comboBox_logSelect.setCurrentText(_translate("MainWindow", "IDL日志")) + self.comboBox_logSelect.setItemText(0, _translate("MainWindow", "IDL日志")) + self.comboBox_logSelect.setItemText(1, _translate("MainWindow", "审计日志")) + self.comboBox_logSelect.setItemText(2, _translate("MainWindow", "维护日志")) + self.comboBox_logSelect.setItemText(3, _translate("MainWindow", "OS串口日志")) + self.comboBox_logSelect.setItemText(4, _translate("MainWindow", "运行状态")) + self.comboBox_logSelect.setItemText(5, _translate("MainWindow", "部件日志")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_readlog), _translate("MainWindow", "日志查看")) self.menu.setTitle(_translate("MainWindow", "文件")) self.actionUpload_log.setText(_translate("MainWindow", "打开")) self.actionUpload_log.setStatusTip(_translate("MainWindow", "上传日志文件到程序中")) diff --git a/src/ZiJin_mergeLog.py b/src/ZiJin_mergeLog.py new file mode 100644 index 0000000..c4d4ea3 --- /dev/null +++ b/src/ZiJin_mergeLog.py @@ -0,0 +1,58 @@ +import os +import utils + +project_root = utils.get_project_root() +cache_dir = os.path.join(project_root, "okd_tmp") +onekeylog_dir = os.path.join(cache_dir, "onekeylog") +extlog_dir = os.path.join(onekeylog_dir, "log") +sollog_dir = os.path.join(extlog_dir, "sollog") +running_dir = os.path.join(onekeylog_dir, "runningdata") +comp_dir = os.path.join(onekeylog_dir, "component") + +# cache_{file} +cache_idl = os.path.join(cache_dir, "merge_idl.log") +cache_audit = os.path.join(cache_dir, "merge_audit.log") +cache_maintenace = os.path.join(cache_dir, "merge_maintenance.log") +cache_console = os.path.join(cache_dir, "os_sol.log") +running_file = os.path.join(running_dir, "rundatainfo.log") +compfile = os.path.join(comp_dir, "component.log") + +def merge_all_log_to_root(): + # 1.融合处理IDL日志, 已在parse_idl中整合, pass + + # 2.融合审计日志 + audit_log = os.path.join(extlog_dir, "audit.log") + utils.merge_logrotate_files(audit_log, 2, cache_audit) + + # 3.融合维护日志 + # 暂时设定为最多解压四份,共合并五份日志 + maintenance_log = os.path.join(extlog_dir, "maintenance.log") + utils.extract_maintenancelog_gz_files(extlog_dir, extlog_dir, 4) + utils.merge_logrotate_files(maintenance_log, 5, cache_maintenace) + + # 4.融合OS串口日志 + sol_log = os.path.join(sollog_dir, "SOLHostCapture.log") + utils.merge_logrotate_files(sol_log, 2, cache_console) + +def get_logfile_path_by_key(key): + match key: + case "log_idl": + logfile = cache_idl + case "log_audit": + logfile = cache_audit + case "log_maintenance": + logfile = cache_maintenace + case "log_osconsole": + logfile = cache_console + case "log_run": + logfile = running_file + case "log_comp": + logfile = compfile + case _ : + logfile = "" + + return logfile + +def program_main(): + merge_all_log_to_root() + return True \ No newline at end of file diff --git a/src/background_task.py b/src/background_task.py index d7887e1..2f69042 100644 --- a/src/background_task.py +++ b/src/background_task.py @@ -37,4 +37,27 @@ class DiagnoseThread(QThread): def stop(self): """停止线程""" self.is_running = False - self.wait() \ No newline at end of file + self.wait() + +class FileReaderThread(QThread): + # 定义信号,用于传递读取到的文本块和完成状态 + text_chunk_ready = pyqtSignal(str) + finished = pyqtSignal() + + def __init__(self, file_path, chunk_size=4096): + super().__init__() + self.file_path = file_path + self.chunk_size = chunk_size # 每次读取的块大小 + + def run(self): + try: + with open(self.file_path, 'r', encoding='utf-8', errors='ignore') as f: + while True: + chunk = f.read(self.chunk_size) # 分块读取 + if not chunk: + break + self.text_chunk_ready.emit(chunk) # 发送文本块到主线程 + self.finished.emit() + except Exception as e: + self.text_chunk_ready.emit(f"读取文件错误: {str(e)}") + self.finished.emit() \ No newline at end of file diff --git a/src/event_handler.py b/src/event_handler.py index d82e698..b2357cc 100644 --- a/src/event_handler.py +++ b/src/event_handler.py @@ -1,7 +1,9 @@ import os +import service from PyQt5.QtWidgets import QFileDialog from PyQt5.QtCore import Qt -from PyQt5.QtGui import QDragEnterEvent, QDropEvent +from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QTextCharFormat +from background_task import FileReaderThread class EventHandler: def __init__(self, main_window): @@ -19,6 +21,8 @@ class EventHandler: self.main_window.graphicsView.wheelEvent = self.zoom_with_mouse_wheel # 窗口 resize 事件 self.main_window.resizeEvent = self.resize_event + # 日志下拉框变化事件 + self.main_window.comboBox_logSelect.currentTextChanged.connect(self.logSelect_on_combo_changed) def upload_file(self): """打开文件选择对话框""" @@ -40,6 +44,27 @@ class EventHandler: self.main_window.file_processor.get_sensorhistory_path(key), self.main_window.parse_status.diag_complete_status ) + + def logview_append_text(self, chunk): + # 在主线程中追加文本 + self.main_window.textBrowser_logView.insertPlainText(chunk) + # 自动滚动到底部 + self.main_window.textBrowser_logView.moveCursor( + self.main_window.textBrowser_logView.textCursor().End) + + def logSelect_on_combo_changed(self, selected_text): + key = self.main_window.comboBox_logSelect_text_dict.get(selected_text) + if key: + filepath = service.get_logfile_path(key) + if not os.path.exists(filepath): + self.main_window.textBrowser_logView.setText("文件不存在/未上传日志") + return + self.main_window.textBrowser_logView.clear() + + self.main_window.reader_thread = FileReaderThread(filepath) + self.main_window.reader_thread.text_chunk_ready.connect(self.logview_append_text) + # self.main_window.reader_thread.finished.connect(self.on_read_finished) + self.main_window.reader_thread.start() def dragEnterEvent(self, event: QDragEnterEvent): """拖入事件:判断是否为文件""" diff --git a/src/main_window.py b/src/main_window.py index 7ccec28..7d0cffd 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -30,10 +30,11 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui_init.init_textbrowser_style() self.ui_init.init_tableView_alert_model() self.ui_init.re_init_time_line_tab() + self.ui_init.init_textbrowser_logview_style() # 事件绑定(按钮、拖放、下拉框) self.event_handler.bind_events() self.event_handler.patch_drag_events() # 将拖放事件绑定到主窗口 - # 初始化下拉框映射字典(跨模块使用,放在主窗口) + # 初始化温度图下拉框映射字典(跨模块使用,放在主窗口) self.combo_box_text_dict = { "ALL_Temp": "all", "CPU_Temp": "cpu", @@ -50,4 +51,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): "OPT1_Temp": "opt1", "OPT2_Temp": "opt2", "OPT3_Temp": "opt3", + } + # 初始化日志窗格下拉框映射字典 + self.reader_thread = None + self.comboBox_logSelect_text_dict = { + "IDL日志": "log_idl", + "审计日志": "log_audit", + "维护日志": "log_maintenance", + "OS串口日志": "log_osconsole", + "运行状态": "log_run", + "部件日志": "log_comp" } \ No newline at end of file diff --git a/src/search_shortcut.py b/src/search_shortcut.py new file mode 100644 index 0000000..bb739dc --- /dev/null +++ b/src/search_shortcut.py @@ -0,0 +1,65 @@ +from PyQt5.QtWidgets import QDialog, QHBoxLayout, QLineEdit, QLabel, QPushButton, QMessageBox +from PyQt5.QtGui import QTextCursor + +class FindDialog(QDialog): + """独立的查找对话框类,用于在文本中查找指定内容""" + def __init__(self, parent=None, text_browser=None): + super().__init__(parent) + self.text_browser = text_browser + self.init_ui() + + def init_ui(self): + self.setWindowTitle("查找") + self.setFixedSize(300, 80) + self.setModal(True) # 模态对话框,阻止对父窗口的操作 + + # 创建布局 + layout = QHBoxLayout() + + # 查找输入框 + self.find_input = QLineEdit() + self.find_input.setPlaceholderText("输入要查找的内容...") + self.find_input.returnPressed.connect(self.find_next) # 回车触发查找 + + # 查找按钮 + self.find_btn = QPushButton("查找下一个") + self.find_btn.clicked.connect(self.find_next) + + # 添加到布局 + layout.addWidget(QLabel("查找:")) + layout.addWidget(self.find_input) + layout.addWidget(self.find_btn) + + self.setLayout(layout) + + # 让输入框获取焦点 + self.find_input.setFocus() + + def find_next(self): + """查找下一个匹配项""" + if not self.text_browser: + return + + # 获取要查找的文本 + find_text = self.find_input.text() + if not find_text: + return + + # 获取当前文本和光标 + document = self.text_browser.document() + cursor = self.text_browser.textCursor() + + # 从当前位置之后查找 + cursor = document.find(find_text, cursor) + + # 如果没找到,从头开始查找 + if cursor.isNull(): + cursor = QTextCursor(document) + cursor = document.find(find_text, cursor) + + # 如果找到匹配项 + if not cursor.isNull(): + self.text_browser.setTextCursor(cursor) + self.text_browser.ensureCursorVisible() # 滚动到可见位置 + else: + QMessageBox.information(self, "查找完成", f"找不到 '{find_text}'") diff --git a/src/service.py b/src/service.py index 6ae4717..707ad2f 100644 --- a/src/service.py +++ b/src/service.py @@ -5,6 +5,7 @@ import ZiJin_parse_sensorhistory as sensorparse import ZiJin_parse_baseinfo as baseinfo import ZiJin_parse_idl as parseidl import ZiJin_parse_event as zijin_event +import ZiJin_mergeLog as mergelog class ServiceStatus(): def __init__(self): @@ -12,6 +13,7 @@ class ServiceStatus(): self.sensorhistory_status = False self.baseinfo_status = False self.parseidl_status = False + self.mergelog_status = False self.eventline_status = False def set_sensorhistory_status(self, status): @@ -37,6 +39,12 @@ class ServiceStatus(): def get_eventline_status(self): return self.eventline_status + + def set_mergelog_status(self, status): + self.mergelog_status = status + + def get_mergelog_status(self): + return self.mergelog_status def app_cache_init(): project_root = utils.get_project_root() @@ -76,6 +84,9 @@ def is_idl_alert_empty(): def get_timeline_event_json(): return zijin_event.get_event_json() +def get_logfile_path(key): + return mergelog.get_logfile_path_by_key(key) + def start_diagnose(parseStatus, filepath, progress_callback=None): """ 执行诊断任务,添加进度回调 @@ -101,10 +112,15 @@ def start_diagnose(parseStatus, filepath, progress_callback=None): parseStatus.set_baseinfo_status(result_baseinfo) if progress_callback: - progress_callback(80, "正在解析告警信息...") + progress_callback(60, "正在解析告警信息...") result_parseidl = parseidl.program_main() parseStatus.set_parseidl_status(result_parseidl) + if progress_callback: + progress_callback(80, "正在整合日志文件...") + result_mergelog = mergelog.program_main() + parseStatus.set_mergelog_status(result_mergelog) + if progress_callback: progress_callback(95, "正在生成时间线...") result_eventline = zijin_event.program_main() diff --git a/src/ui_initializer.py b/src/ui_initializer.py index 161bd6d..6d1ff77 100644 --- a/src/ui_initializer.py +++ b/src/ui_initializer.py @@ -1,8 +1,9 @@ -from PyQt5.QtWidgets import QVBoxLayout, QGraphicsScene -from PyQt5.QtGui import QPainter, QStandardItemModel +from PyQt5.QtWidgets import QVBoxLayout, QGraphicsScene, QShortcut +from PyQt5.QtGui import (QPainter, QStandardItemModel, QTextOption, QKeySequence, + QTextCharFormat, QBrush, QColor) from PyQt5.QtCore import Qt import timelineEvent - +from search_shortcut import FindDialog class UIInitializer: def __init__(self, main_window): self.main_window = main_window # 持有主窗口引用 @@ -79,4 +80,26 @@ class UIInitializer: # 列宽自适应 self.main_window.tableView_alert.horizontalHeader().setSectionResizeMode( self.main_window.tableView_alert.horizontalHeader().ResizeToContents - ) \ No newline at end of file + ) + + def show_find_dialog(self): + """显示查找对话框""" + # 如果文本浏览器有内容才显示查找对话框 + if self.main_window.textBrowser_logView.toPlainText(): + dialog = FindDialog(self.main_window, self.main_window.textBrowser_logView) + dialog.exec_() + + def init_textbrowser_logview_style(self): + # 关键设置:禁用自动换行,启用横向滚动条 + self.main_window.textBrowser_logView.setWordWrapMode(QTextOption.NoWrap) + + # 设置字体为等宽字体,更适合查看日志 + font = self.main_window.textBrowser_logView.font() + font.setFamily("Consolas") # Windows系统 + # font.setFamily("Monaco") # macOS系统 + # font.setFamily("Monospace") # Linux系统 + self.main_window.textBrowser_logView.setFont(font) + + # 设置Ctrl+F快捷键 + self.main_window.textBrowser_logView.find_shortcut = QShortcut(QKeySequence("Ctrl+F"), self.main_window) + self.main_window.textBrowser_logView.find_shortcut.activated.connect(self.show_find_dialog) \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 5971cb7..b83c718 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,7 +3,9 @@ import tarfile import shutil import stat import re +import gzip from datetime import datetime +from pathlib import Path from PyQt5.QtWidgets import QMessageBox def show_error_message(parent, file_path): @@ -397,4 +399,53 @@ def parse_idllog_line(log_line): except Exception as e: print(f"解析日志行时出错: {str(e)}") - return None \ No newline at end of file + return None + +def extract_maintenancelog_gz_files(input_dir, output_dir, max_files=10): + """ + 解压指定目录下的maintenancelog.1.gz到log.max_files.gz文件到目标目录 + + 参数: + input_dir: 压缩文件所在的目录路径 + output_dir: 解压后文件的保存目录路径 + max_files: 最大文件编号,默认为10 + """ + # 确保输入输出目录存在 + input_path = Path(input_dir) + output_path = Path(output_dir) + + # 创建输出目录(如果不存在) + output_path.mkdir(parents=True, exist_ok=True) + + # 检查输入目录是否存在 + if not input_path.exists() or not input_path.is_dir(): + print(f"错误:输入目录 '{input_path}' 不存在或不是一个目录") + return + + for i in range(1, max_files + 1): + # 压缩文件路径 + gz_filename = input_path / f"maintenance.log.{i}.gz" + + # 检查文件是否存在 + if not gz_filename.exists() or not gz_filename.is_file(): + print(f"文件 {gz_filename} 不存在,跳过") + continue + + # 解压后的文件路径 + output_filename = output_path / f"maintenance.log.{i}" + + try: + # 打开压缩文件并解压 + with gzip.open(gz_filename, 'rb') as f_in: + with open(output_filename, 'wb') as f_out: + # 分块读取写入,处理大文件更高效 + while True: + chunk = f_in.read(1024 * 1024) # 1MB块 + if not chunk: + break + f_out.write(chunk) + + print(f"成功解压: {gz_filename} -> {output_filename}") + + except Exception as e: + print(f"解压 {gz_filename} 时出错: {str(e)}") \ No newline at end of file