diff --git a/resource/OneKeyLogDiag_v2.ui b/resource/OneKeyLogDiag_v2.ui index 6a28a90..66c6741 100644 --- a/resource/OneKeyLogDiag_v2.ui +++ b/resource/OneKeyLogDiag_v2.ui @@ -36,7 +36,7 @@ - + diff --git a/src/backend/back_service.py b/src/backend/back_service.py index 86b14f2..c7c81d0 100644 --- a/src/backend/back_service.py +++ b/src/backend/back_service.py @@ -1,5 +1,6 @@ import os import importlib +from common import record_log class AppService: def __init__(self): @@ -95,10 +96,16 @@ def get_project_logname_list(key): return result -def do_proj_parse(key="app"): +def do_proj_parse(parseStatus, filepath, key="app", progress_callback = None): porj_package = "proj_" + key pHandlers = dynamic_import_lib(porj_package) if hasattr(pHandlers, "start_parse"): + # Step 1 . 解压日志文件, 并记录到历史信息 + + # Step 2 . 调用具体handler进行自定义解析 pHandlers.start_parse() - \ No newline at end of file + else: + record_log("ERROR", f"Project : {key} | No 'start_parse' function found, failed") + if progress_callback: + progress_callback(-1, "处理出错") \ No newline at end of file diff --git a/src/common/__init__.py b/src/common/__init__.py index 2548c29..326c0b0 100644 --- a/src/common/__init__.py +++ b/src/common/__init__.py @@ -15,7 +15,8 @@ from .file_tool import( from .cache_mgmt import( clean_app_cache, - clean_running_log + clean_running_log, + add_onekeylog_info_cache ) # From ._globals @@ -28,4 +29,4 @@ __all__ += [ record_log ] __all__ += [ force_empty_folder, add_to_json_array, get_user_appdata_path ] # From .cache_mgmt -__all__ += [ clean_app_cache, clean_running_log] \ No newline at end of file +__all__ += [ clean_app_cache, clean_running_log, add_onekeylog_info_cache ] \ No newline at end of file diff --git a/src/common/cache_mgmt.py b/src/common/cache_mgmt.py index 685bb8d..e046905 100644 --- a/src/common/cache_mgmt.py +++ b/src/common/cache_mgmt.py @@ -1,5 +1,6 @@ import os import time +import tarfile from ._globals import cache_dir from .file_tool import force_empty_folder, add_to_json_array, get_user_appdata_path @@ -21,7 +22,7 @@ def app_cache_create(): os.makedirs(app_plugin_path) record_log("Info", f"Create Dir:{app_plugin_path}") -def add_onekeylog_info_cache(log_name): +def add_onekeylog_info_cache(log_name, proj_key): """ 添加一份新日志时触发这个动作, 将日志名称以及解析日期全部记录到 parse_history.json @@ -35,6 +36,7 @@ def add_onekeylog_info_cache(log_name): # 生成新的记录Dict new_entry = { + "project" : proj_key, "filename" : log_name, "timestamp" : current_time } @@ -44,6 +46,41 @@ def add_onekeylog_info_cache(log_name): if not result_b: record_log("ERROR", "Fail to add parse history cache") +def unzip_log(tar_path, extract_path='.'): + """ + 解压 tar.gz 文件到指定目录 + + 参数: + tar_path (str): tar.gz 文件的路径 + extract_path (str): 解压目标目录,默认为当前目录 + """ + try: + # 检查文件是否存在 + if not os.path.exists(tar_path): + raise FileNotFoundError(f"文件不存在: {tar_path}") + + # 创建解压目录(如果不存在) + os.makedirs(extract_path, exist_ok=True) + + # 打开 tar.gz 文件并解压 + with tarfile.open(tar_path, "r:gz") as tar: + # 列出所有文件(可选) + print(f"解压文件列表:") + for member in tar.getmembers(): + print(f"- {member.name}") + + # 解压所有文件到目标目录 + tar.extractall(path=extract_path) + print(f"\n成功解压到: {os.path.abspath(extract_path)}") + + except tarfile.TarError as e: + print(f"tar 文件处理错误: {e}") + except Exception as e: + print(f"解压失败: {e}") + +def send_log_to_cache(filepath, key): + pass + def clean_app_cache(): force_empty_folder(cache_dir) diff --git a/src/frontend/background_task.py b/src/frontend/background_task.py new file mode 100644 index 0000000..0701b28 --- /dev/null +++ b/src/frontend/background_task.py @@ -0,0 +1,64 @@ +from PyQt6.QtCore import QThread, pyqtSignal +from backend import do_proj_parse + +class DiagnoseThread(QThread): + """后台线程,用于执行耗时的诊断任务""" + # 定义信号:更新进度(值, 消息)、任务完成(是否成功, 消息) + progress_updated = pyqtSignal(int, str) + task_completed = pyqtSignal(bool, str) + + def __init__(self, parse_status, file_path, key): + super().__init__() + self.parse_status = parse_status + self.file_path = file_path + self.is_running = True + self.proj_key = key + + def run(self): + """线程执行函数:执行耗时任务""" + try: + self.progress_updated.emit(1, "正在初始化解析...") + + # 执行耗时任务 + do_proj_parse(self.parse_status, self.file_path, self.proj_key, progress_callback=self.update_progress) + + # 确保最终进度为100% + self.progress_updated.emit(100, "处理完成") + self.task_completed.emit(True, "日志解析任务已完成") + except Exception as e: + # 出错时也允许关闭对话框 + self.progress_updated.emit(100, f"处理失败:{str(e)}") + self.task_completed.emit(False, f"处理失败:{str(e)}") + + def update_progress(self, value, message): + """接收service层的进度更新""" + if 0 <= value <= 100 and self.is_running: + self.progress_updated.emit(value, message) + + def stop(self): + """停止线程""" + self.is_running = False + 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/frontend/event_handler.py b/src/frontend/event_handler.py index f3af934..c488e02 100644 --- a/src/frontend/event_handler.py +++ b/src/frontend/event_handler.py @@ -1,12 +1,17 @@ +import os +from PyQt6.QtWidgets import QFileDialog +from PyQt6.QtGui import QDragEnterEvent, QDropEvent from common import record_log class EventHandler: def __init__(self, main_window): self.main_window = main_window # 持有主窗口引用 + self.file_processor = main_window.file_processor def bind_event(self): record_log("INFO", "Start Bind Event") # 菜单项事件绑定 + self.main_window.action_uploadFile.triggered.connect(self.upload_file_in_resource) self.main_window.action_cleanCache.triggered.connect(self.on_click_clean_cache) self.main_window.action_cleanLog.triggered.connect(self.on_click_clean_running_log) @@ -40,15 +45,42 @@ class EventHandler: def on_click_clean_running_log(self): from common import clean_running_log - self.main_window.textEdit_console.append("系统信息: Clean apprunning.log") + self.main_window.textBrowser_console.insertPlainText("系统信息: Clean apprunning.log") clean_running_log() def on_click_clean_cache(self): from common import clean_app_cache - self.main_window.textEdit_console.append("系统信息: Clean all app cache") + self.main_window.textBrowser_console.insertPlainText("系统信息: Clean all app cache") clean_app_cache() - - + + def upload_file_in_resource(self): + """打开文件选择对话框""" + file_path, _ = QFileDialog.getOpenFileName( + self.main_window, + "选择文件", + os.getcwd(), + "所有文件 (*);;日志压缩文件 (*.gz)" + ) + if file_path: + self.file_processor.process_uploaded_file(file_path) + + def dragEnterEvent(self, event: QDragEnterEvent): + """拖入事件:判断是否为文件""" + if event.mimeData().hasUrls(): + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event: QDropEvent): + """放下事件:处理拖放的文件""" + file_paths = [url.toLocalFile() for url in event.mimeData().urls()] + for file_path in file_paths: + self.file_processor.process_uploaded_file(file_path) + + def patch_drag_event(self): + """将拖放事件绑定到主窗口(需在主窗口初始化时调用)""" + self.main_window.dragEnterEvent = self.dragEnterEvent + self.main_window.dropEvent = self.dropEvent \ No newline at end of file diff --git a/src/frontend/file_processor.py b/src/frontend/file_processor.py index dd3899d..e45bce2 100644 --- a/src/frontend/file_processor.py +++ b/src/frontend/file_processor.py @@ -1,3 +1,91 @@ +import os +from .progress_diaglog import ProgressDialog +from .background_task import DiagnoseThread +from common import record_log +from .utils import (show_upload_error_message, + show_critical_message, + show_info_message) + class FileProcessor: def __init__(self, main_window): - self.main_window = main_window \ No newline at end of file + self.main_window = main_window + self.serviceStatus = main_window.serviceStatus + + def process_uploaded_file(self, file_path): + record_log("INFO", f"Upload file : {file_path}") + current_project_key = self.main_window.comboBox_proj_select.currentText() + + # 格式校验 + if not self.is_valid_file_format(file_path): + show_upload_error_message(self.main_window, file_path) + return + + try: + # 显示文件信息 + file_name = os.path.basename(file_path) + file_size = os.path.getsize(file_path) / 1024 + info = ( + f"已上传文件:\n" + f"文件名:{file_name}\n" + f"路径:{file_path}\n" + f"大小:{file_size:.2f} KB\n" + f"-------------------------------------------" + "\n" + ) + self.main_window.textBrowser_console.insertPlainText(info) + + # 初始化进度对话框 + self.progress_dialog = ProgressDialog( + parent=self.main_window, + title="正在处理文件", + label_text="准备开始处理..." + ) + + # 创建并配置后台线程 + self.diagnose_thread = DiagnoseThread(self.serviceStatus, file_path, current_project_key) + self.diagnose_thread.progress_updated.connect(self.progress_dialog.update_progress) + self.diagnose_thread.task_completed.connect(self.on_diagnose_completed) + + # 显示进度条并启动线程 + self.progress_dialog.show() + self.diagnose_thread.start() + + except Exception as e: + show_critical_message(self.main_window, "处理失败", f"文件处理出错:{str(e)}") + + def is_valid_file_format(self, file_path): + # 原有实现... + _, ext = os.path.splitext(file_path) + if ext.lower() == '.gz': + base, ext2 = os.path.splitext(os.path.splitext(file_path)[0]) + return ext2.lower() == '.tar' + return False + + def _update_view_after_parse(self): + """ + 解析任务完成, 将数据同步到视图中 + """ + pass + + def on_diagnose_completed(self, success, message): + """处理诊断任务完成后的逻辑""" + if hasattr(self, 'progress_dialog') and self.progress_dialog: + # 明确允许对话框关闭 + self.progress_dialog.set_allow_close(True) + # 关闭对话框 + self.progress_dialog.close() + # 释放引用 + self.progress_dialog = None + + # 显示结果信息 + self.main_window.textBrowser_console.insertPlainText(f"{message}\n") + + if success: + # 处理成功,更新视图 + self._update_view_after_parse() + show_info_message(self.main_window, "成功", "文件解析完成") + else: + # 处理失败,显示错误 + show_critical_message(self.main_window, "失败", message) + + # 清理线程引用 + self.diagnose_thread = None \ No newline at end of file diff --git a/src/frontend/main_window.py b/src/frontend/main_window.py index 293e834..180729a 100644 --- a/src/frontend/main_window.py +++ b/src/frontend/main_window.py @@ -36,9 +36,19 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui_init.init_comboBox_project_select() self.ui_init.init_comboBox_temperature_select() self.ui_init.init_comboBox_logreader_select() - self.ui_init.init_textEdit_console_style() + self.ui_init.init_textBrowser_console_style() self.ui_init.init_textBrowser_baseinfo_style() self.ui_init.init_textBrowser_logreader_style() # Event_Handler - self.event_handler.bind_event() \ No newline at end of file + self.event_handler.bind_event() + self.event_handler.patch_drag_event() + + # 完成UI初始化 + record_log("INFO", "Complete UI Init") + + def reinit_main_ui(self): + """ + 在使用日志更新时重新刷新界面整体布局 + """ + record_log("INFO", "Triggered reinit main window") \ No newline at end of file diff --git a/src/frontend/onekeydiag_ui.py b/src/frontend/onekeydiag_ui.py index b5ddcbf..4bac610 100644 --- a/src/frontend/onekeydiag_ui.py +++ b/src/frontend/onekeydiag_ui.py @@ -29,9 +29,9 @@ class Ui_MainWindow(object): self.comboBox_proj_select = QtWidgets.QComboBox(parent=self.tab_console) self.comboBox_proj_select.setObjectName("comboBox_proj_select") self.verticalLayout_4.addWidget(self.comboBox_proj_select) - self.textEdit_console = QtWidgets.QTextEdit(parent=self.tab_console) - self.textEdit_console.setObjectName("textEdit_console") - self.verticalLayout_4.addWidget(self.textEdit_console) + self.textBrowser_console = QtWidgets.QTextBrowser(parent=self.tab_console) + self.textBrowser_console.setObjectName("textBrowser_console") + self.verticalLayout_4.addWidget(self.textBrowser_console) self.tabWidget.addTab(self.tab_console, "") self.tab_baseinfo = QtWidgets.QWidget() self.tab_baseinfo.setObjectName("tab_baseinfo") diff --git a/src/frontend/progress_diaglog.py b/src/frontend/progress_diaglog.py new file mode 100644 index 0000000..66d24d3 --- /dev/null +++ b/src/frontend/progress_diaglog.py @@ -0,0 +1,46 @@ +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QProgressBar, QLabel +from PyQt6.QtCore import Qt, pyqtSignal + +class ProgressDialog(QDialog): + """进度条对话框,支持程序控制自动关闭""" + def __init__(self, parent=None, title="处理中", label_text="正在处理,请稍候..."): + super().__init__(parent) + self.setWindowTitle(title) + self.setModal(True) # 模态对话框 + self.setFixedSize(400, 120) + self.allow_close = False # 新增:允许关闭标记 + + # 布局 + layout = QVBoxLayout() + + # 提示文本 + self.label = QLabel(label_text) + layout.addWidget(self.label) + + # 进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) + + def update_progress(self, value, message=None): + """更新进度值和提示信息""" + self.progress_bar.setValue(value) + if message: + self.label.setText(message) + # 当进度完成时,允许关闭 + if value >= 100: + self.allow_close = True + + def set_allow_close(self, allow): + """设置是否允许关闭对话框""" + self.allow_close = allow + + def closeEvent(self, event): + """根据允许关闭标记决定是否关闭""" + if self.allow_close: + event.accept() # 允许关闭 + else: + event.ignore() # 禁止关闭 \ No newline at end of file diff --git a/src/frontend/ui_initializer.py b/src/frontend/ui_initializer.py index e2f54bb..98ff15f 100644 --- a/src/frontend/ui_initializer.py +++ b/src/frontend/ui_initializer.py @@ -38,19 +38,18 @@ class UIInitializer: if logname_count > 0: self.main_window.comboBox_logreader.addItems(logname_list) - def init_textEdit_console_style(self): - # self.main_window.textEdit_console.setStyleSheet(""" - # QTextBrowser { - # font-family: 'SimHei'; - # font-size: 20px; - # color: #2c3e50; - # background-color: #f8f9fa; - # border: 1px solid #ddd; - # border-radius: 4px; - # padding: 8px; - # } - # """) - pass + def init_textBrowser_console_style(self): + self.main_window.textBrowser_console.setStyleSheet(""" + QTextBrowser { + font-family: 'SimHei'; + font-size: 20px; + color: #2c3e50; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px; + } + """) def init_textBrowser_baseinfo_style(self): self.main_window.textBrowser_baseinfo.setStyleSheet(""" diff --git a/src/frontend/utils.py b/src/frontend/utils.py new file mode 100644 index 0000000..2d84a92 --- /dev/null +++ b/src/frontend/utils.py @@ -0,0 +1,35 @@ +from PyQt6.QtWidgets import QMessageBox + +def show_upload_error_message(parent, file_path): + """显示文件格式错误提示弹窗""" + QMessageBox.critical( + parent, + "文件格式错误", + f"不支持的文件格式:\n{file_path}\n\n请上传.tar.gz格式的压缩包。" + ) + +def show_critical_message(parent, title, message): + """显示通用错误提示弹窗""" + QMessageBox.critical( + parent, + title, + message + ) + +def show_info_message(parent, title, message): + """显示信息提示弹窗""" + QMessageBox.information( + parent, + title, + message + ) + +def show_question_message(parent, title, message): + """显示询问提示弹窗,返回用户选择(Yes/No)""" + return QMessageBox.question( + parent, + title, + message, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) \ No newline at end of file