2026-03-02 14:29:58 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import re
|
|
|
|
|
|
import time
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
import queue
|
|
|
|
|
|
from PyQt6 import *
|
|
|
|
|
|
from PyQt6.QtCore import *
|
|
|
|
|
|
from PyQt6.QtGui import *
|
|
|
|
|
|
from PyQt6.QtWidgets import *
|
|
|
|
|
|
from ui.Ui_logForm import Ui_logForm
|
|
|
|
|
|
from logModel.logManager import logManager
|
|
|
|
|
|
from projectModel.projectManager import projectManager
|
|
|
|
|
|
from common import common
|
|
|
|
|
|
from config import config
|
|
|
|
|
|
from logs import log
|
|
|
|
|
|
import copy
|
|
|
|
|
|
|
|
|
|
|
|
class LogThread(QThread):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.levels = {"DEBUG": ["0"], "INFO": ["0", "1"], "WARNING": ["0", "1", "2"], "ERROR": ["0", "1", "2", "3"]}
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.logQueue = queue.Queue()
|
|
|
|
|
|
self.maxCount = 500
|
|
|
|
|
|
self.msgDataMap = {}
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 增量标记:记录每个 id 上次渲染后新增了多少条
|
|
|
|
|
|
self.newCountMap = {}
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def run(self):
|
|
|
|
|
|
while True:
|
|
|
|
|
|
if not self.logQueue.empty():
|
|
|
|
|
|
msgData = self.logQueue.get()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
labs = self.levels.get(msgData.get("level", "DEBUG"), ["0"])
|
|
|
|
|
|
for lab in labs:
|
2026-03-02 14:29:58 +08:00
|
|
|
|
try:
|
2026-04-17 15:31:41 +08:00
|
|
|
|
all_key = lab
|
|
|
|
|
|
id_key = lab + str(msgData.get("tag", ""))
|
2026-03-02 14:29:58 +08:00
|
|
|
|
msgStr = f"<span style='color:{msgData['color']};'>{msgData['msg']}</span><br>"
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 更新 id 日志
|
|
|
|
|
|
if id_key not in self.msgDataMap:
|
|
|
|
|
|
self.msgDataMap[id_key] = []
|
|
|
|
|
|
self.msgDataMap[id_key].append(msgStr)
|
|
|
|
|
|
if len(self.msgDataMap[id_key]) > self.maxCount:
|
|
|
|
|
|
self.msgDataMap[id_key].pop(0)
|
|
|
|
|
|
|
|
|
|
|
|
# 更新 all 日志
|
|
|
|
|
|
if all_key not in self.msgDataMap:
|
|
|
|
|
|
self.msgDataMap[all_key] = []
|
|
|
|
|
|
self.msgDataMap[all_key].append(msgStr)
|
|
|
|
|
|
if len(self.msgDataMap[all_key]) > self.maxCount:
|
|
|
|
|
|
self.msgDataMap[all_key].pop(0)
|
|
|
|
|
|
|
|
|
|
|
|
# 标记有新增
|
|
|
|
|
|
self.newCountMap[id_key] = self.newCountMap.get(id_key, 0) + 1
|
|
|
|
|
|
self.newCountMap[all_key] = self.newCountMap.get(all_key, 0) + 1
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(e)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
else:
|
|
|
|
|
|
QThread.msleep(10) # 队列空时休眠,降低 CPU
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
|
|
class LogForm(QWidget):
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ui = Ui_logForm()
|
|
|
|
|
|
self.ui.setupUi(self)
|
|
|
|
|
|
self.endStr = "---END"
|
|
|
|
|
|
self.timer = QTimer()
|
|
|
|
|
|
self.isScrollBottom = True
|
|
|
|
|
|
self.currentProId = ""
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.refreshCount = config.data["log"].get("refreshCount", 100)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.logThread = LogThread()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.levels = {"DEBUG": ["0"], "INFO": ["0", "1"], "WARNING": ["0", "1", "2"], "ERROR": ["0", "1", "2", "3"]}
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.levelIndex = 0
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.refreshTime = config.data["log"].get("refreshTime", 100)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.tagText = ""
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.lastRenderedCount = 0 # 上次渲染的总条数
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.logArray = []
|
|
|
|
|
|
self.timer.timeout.connect(self.setLogPlainText)
|
|
|
|
|
|
logManager.logMsgData.connect(self.addLogMsgData, type=Qt.ConnectionType.UniqueConnection)
|
|
|
|
|
|
self.ui.pbOpen.clicked.connect(self.openLogPath)
|
|
|
|
|
|
self.ui.pbClear.clicked.connect(self.clearLog)
|
|
|
|
|
|
self.ui.cbLevel.currentIndexChanged.connect(self.comboxChange)
|
|
|
|
|
|
self.ui.cbTag.currentIndexChanged.connect(self.comboxChange)
|
|
|
|
|
|
self.ui.pbLock.clicked.connect(self.changeIsScrollBottom)
|
|
|
|
|
|
self.ui.plainTextEdit.verticalScrollBar().valueChanged.connect(self.scrollbarValueChanged)
|
|
|
|
|
|
projectManager.sig_projectChanged.connect(self.loadLog)
|
|
|
|
|
|
self.timer.start(self.refreshTime)
|
|
|
|
|
|
self.logThread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def clearLog(self):
|
|
|
|
|
|
self.ui.plainTextEdit.clear()
|
|
|
|
|
|
self.logThread.msgDataMap = {}
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.logThread.newCountMap = {}
|
|
|
|
|
|
self.lastRenderedCount = 0
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
|
|
def changeIsScrollBottom(self):
|
|
|
|
|
|
self.isScrollBottom = not self.isScrollBottom
|
|
|
|
|
|
if self.isScrollBottom:
|
|
|
|
|
|
self.scrollToBottom()
|
|
|
|
|
|
self.ui.pbLock.setIcon(QIcon(":/resource/lock.svg"))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.ui.pbLock.setIcon(QIcon(":/resource/unlock.svg"))
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def nowStr(self):
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
ret = now.strftime('%H:%M:%S.') + f"{now.microsecond // 1000:03d}"
|
|
|
|
|
|
return ret
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
|
|
def _getCurrentId(self):
|
|
|
|
|
|
"""获取当前筛选条件对应的 id"""
|
|
|
|
|
|
level_index = self.ui.cbLevel.currentIndex()
|
|
|
|
|
|
tag_text = str(self.ui.cbTag.currentText())
|
|
|
|
|
|
if tag_text == "ALL":
|
|
|
|
|
|
return str(level_index)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return str(level_index) + tag_text
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def searchType(self):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
"""切换筛选条件时全量重绘"""
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.timer.stop()
|
|
|
|
|
|
self.ui.plainTextEdit.clear()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.lastRenderedCount = 0
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
current_id = self._getCurrentId()
|
|
|
|
|
|
if current_id in self.logThread.msgDataMap:
|
|
|
|
|
|
htmlText = "".join(self.logThread.msgDataMap[current_id])
|
|
|
|
|
|
self.ui.plainTextEdit.appendHtml(htmlText)
|
|
|
|
|
|
self.lastRenderedCount = len(self.logThread.msgDataMap[current_id])
|
|
|
|
|
|
# 清除新增标记
|
|
|
|
|
|
self.logThread.newCountMap[current_id] = 0
|
|
|
|
|
|
|
|
|
|
|
|
self.scrollToBottom()
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.timer.start(self.refreshTime)
|
|
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
def appendColoredText(self, text, font_color):
|
2026-03-02 14:29:58 +08:00
|
|
|
|
fmt = QTextCharFormat()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
fmt.setForeground(QBrush(font_color))
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.ui.plainTextEdit.mergeCurrentCharFormat(fmt)
|
|
|
|
|
|
self.ui.plainTextEdit.appendPlainText(text)
|
|
|
|
|
|
|
|
|
|
|
|
def setLogPlainText(self):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
"""增量更新:只追加新增的日志,不全量重绘"""
|
2026-03-02 14:29:58 +08:00
|
|
|
|
try:
|
2026-04-17 15:31:41 +08:00
|
|
|
|
current_id = self._getCurrentId()
|
|
|
|
|
|
|
|
|
|
|
|
if current_id not in self.logThread.msgDataMap:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
new_count = self.logThread.newCountMap.get(current_id, 0)
|
|
|
|
|
|
if new_count <= 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
data_list = self.logThread.msgDataMap[current_id]
|
|
|
|
|
|
total = len(data_list)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果数据被截断(超过 maxCount),需要全量重绘
|
|
|
|
|
|
if self.lastRenderedCount > total:
|
|
|
|
|
|
self.ui.plainTextEdit.clear()
|
|
|
|
|
|
htmlText = "".join(data_list)
|
|
|
|
|
|
self.ui.plainTextEdit.appendHtml(htmlText)
|
|
|
|
|
|
self.lastRenderedCount = total
|
2026-03-02 14:29:58 +08:00
|
|
|
|
else:
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 只追加新增部分
|
|
|
|
|
|
new_items = data_list[self.lastRenderedCount:]
|
|
|
|
|
|
if new_items:
|
|
|
|
|
|
newHtml = "".join(new_items)
|
|
|
|
|
|
# 使用 moveCursor 追加到末尾,避免 clear
|
|
|
|
|
|
cursor = self.ui.plainTextEdit.textCursor()
|
|
|
|
|
|
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
|
|
|
|
cursor.insertHtml(newHtml)
|
|
|
|
|
|
self.lastRenderedCount = total
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
# 重置新增计数
|
|
|
|
|
|
self.logThread.newCountMap[current_id] = 0
|
|
|
|
|
|
|
|
|
|
|
|
self.scrollToBottom()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
log.error(f"setLogPlainText exception: {e}")
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
|
|
def comboxChange(self, index):
|
|
|
|
|
|
self.searchType()
|
|
|
|
|
|
|
|
|
|
|
|
def openLogPath(self):
|
|
|
|
|
|
os.startfile(projectManager.getLogPath())
|
|
|
|
|
|
|
|
|
|
|
|
def scrollToBottom(self):
|
|
|
|
|
|
if self.isScrollBottom:
|
|
|
|
|
|
self.ui.plainTextEdit.verticalScrollBar().setValue(self.ui.plainTextEdit.verticalScrollBar().maximum())
|
|
|
|
|
|
|
|
|
|
|
|
def scrollbarValueChanged(self, value):
|
|
|
|
|
|
scrollbar = self.ui.plainTextEdit.verticalScrollBar()
|
|
|
|
|
|
max_value = scrollbar.maximum()
|
|
|
|
|
|
current_value = scrollbar.value()
|
|
|
|
|
|
|
|
|
|
|
|
if current_value == max_value:
|
|
|
|
|
|
self.isScrollBottom = True
|
|
|
|
|
|
self.ui.pbLock.setIcon(QIcon(":/resource/lock.svg"))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.isScrollBottom = False
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.ui.pbLock.setIcon(QIcon(":/resource/unlock.svg"))
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
|
|
def loadLog(self, proId):
|
|
|
|
|
|
self.clearLog()
|
|
|
|
|
|
logFilePath = logManager.loadProLog(proId)
|
|
|
|
|
|
try:
|
2026-04-17 15:31:41 +08:00
|
|
|
|
if os.path.exists(logFilePath):
|
2026-03-02 14:29:58 +08:00
|
|
|
|
with open(logFilePath, "r", encoding="utf-8") as f:
|
|
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
log_entries = file_content.strip().split(f'{self.endStr}')
|
|
|
|
|
|
endCount = self.refreshCount
|
2026-04-17 15:31:41 +08:00
|
|
|
|
if self.refreshCount > len(log_entries):
|
2026-03-02 14:29:58 +08:00
|
|
|
|
endCount = 0
|
|
|
|
|
|
for entry in log_entries[-endCount:]:
|
|
|
|
|
|
if entry:
|
|
|
|
|
|
msgData = {}
|
|
|
|
|
|
msgData["time"] = ""
|
|
|
|
|
|
msgData["level"] = "DEBUG"
|
|
|
|
|
|
msgData["msg"] = ""
|
|
|
|
|
|
msgData["color"] = "blue"
|
|
|
|
|
|
msgData["tag"] = ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
log_entry = json.loads(entry)
|
|
|
|
|
|
msgData["time"] = log_entry["time"]
|
|
|
|
|
|
msgData["level"] = log_entry["level"]
|
|
|
|
|
|
msgData["color"] = log_entry["color"]
|
|
|
|
|
|
msgData["msg"] = log_entry["msg"]
|
|
|
|
|
|
msgData["tag"] = log_entry["tag"]
|
|
|
|
|
|
msgData["searchText"] = common.level2Search(msgData["level"])
|
|
|
|
|
|
except:
|
|
|
|
|
|
print("invalid log line:", entry)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.addLogMsg(msgData)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.loadFinish()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
log.error(f"log exception: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def loadFinish(self):
|
|
|
|
|
|
self.scrollToBottom()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
|
|
def addLogMsg(self, msgData):
|
|
|
|
|
|
"""加载历史日志时直接放入 LogThread 队列"""
|
|
|
|
|
|
tag = msgData.get("tag", "")
|
|
|
|
|
|
if tag:
|
|
|
|
|
|
msgData["tag"] = str(tag)
|
|
|
|
|
|
if self.ui.cbTag.findText(str(tag)) == -1:
|
|
|
|
|
|
self.ui.cbTag.addItem(str(tag))
|
|
|
|
|
|
self.logThread.logQueue.put(msgData)
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
|
def addLogMsgData(self, msgData):
|
2026-04-17 15:31:41 +08:00
|
|
|
|
"""实时日志信号处理"""
|
2026-03-02 14:29:58 +08:00
|
|
|
|
tag = ""
|
2026-04-17 15:31:41 +08:00
|
|
|
|
if msgData.get("tag") is not None:
|
2026-03-02 14:29:58 +08:00
|
|
|
|
tag = str(msgData["tag"])
|
|
|
|
|
|
msgData["tag"] = tag
|
2026-04-17 15:31:41 +08:00
|
|
|
|
if self.ui.cbTag.findText(tag) == -1:
|
2026-03-02 14:29:58 +08:00
|
|
|
|
self.ui.cbTag.addItem(tag)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
self.logThread.logQueue.put(msgData)
|