268 lines
9.4 KiB
Python
268 lines
9.4 KiB
Python
#!/opt/homebrew/bin/python3
|
|
# -*- coding:utf-8 -*-
|
|
import os
|
|
import json
|
|
import queue
|
|
from PyQt6 import *
|
|
from PyQt6.QtCore import *
|
|
from logs import log
|
|
from typing import Union
|
|
from projectModel.projectManager import projectManager
|
|
|
|
# class LogWorker(QObject):
|
|
# logMsg = pyqtSignal(dict)
|
|
# logMsgData = pyqtSignal(dict)
|
|
# def __init__(self):
|
|
# super().__init__()
|
|
# self.running = False
|
|
# self.logBuffer = queue.Queue()
|
|
|
|
# self.endStr = "---END"
|
|
# def start(self):
|
|
# self.running = True
|
|
# while self.running:
|
|
# try:
|
|
# if not self.logBuffer.empty():
|
|
# data = self.logBuffer.get()
|
|
# data["time"] = QDateTime.currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz")
|
|
# data["msg"] = data["msg"].replace("\n", " ")
|
|
# self.appendLogFile(data)
|
|
# self.logMsgData.emit(data)
|
|
# else:
|
|
# QThread.msleep(1)
|
|
|
|
# except Exception as e:
|
|
# log.info(f"Serial exception: {e}")
|
|
# finally:
|
|
# pass
|
|
# def appendLogFile(self, data):
|
|
# logPath = projectManager.getLogPath()
|
|
# if logPath == "":
|
|
# return
|
|
# time = data["time"]
|
|
# level = data["level"]
|
|
# msg = data["msg"].replace("\n", "++")
|
|
# color = data["color"]
|
|
# tag = "tag" in data and data["tag"] or ""
|
|
# log_data = {
|
|
# "time": time,
|
|
# "level": level,
|
|
# "msg": msg,
|
|
# "color": color,
|
|
# "tag": tag
|
|
# }
|
|
# with open(os.path.join(logPath, f"log{QDateTime.currentDateTime().toString('yyyy-MM-dd')}.txt"), "a+",encoding='utf-8') as f:
|
|
# json.dump(log_data, f, ensure_ascii=False)
|
|
# f.write(f'{self.endStr}\n')
|
|
# if tag != "":
|
|
# with open(os.path.join(logPath, f"log-{tag}.txt"), "a+",encoding='utf-8') as f:
|
|
# json.dump(log_data, f, ensure_ascii=False)
|
|
# f.write(f'{self.endStr}\n')
|
|
# def stop(self):
|
|
# self.running = False
|
|
class LogWorker(QObject):
|
|
logMsg = pyqtSignal(dict) # 实时日志信号
|
|
logMsgData = pyqtSignal(dict) # 实时日志数据信号
|
|
errorOccurred = pyqtSignal(str) # 新增错误信号
|
|
|
|
def __init__(self, localDateTime=None):
|
|
super().__init__()
|
|
self.running = False
|
|
self.logBuffer = queue.Queue(maxsize=2000) # 增加队列容量
|
|
#获取当前时间
|
|
self.localDateTime = localDateTime
|
|
self.endStr = "---END"
|
|
self.file_handles = {} # 文件句柄缓存
|
|
self.mutex = QMutex() # 线程安全锁
|
|
self.batch_size = 50 # 批量处理数量
|
|
self._init_file_rotation()
|
|
|
|
def _init_file_rotation(self):
|
|
"""日志轮转配置"""
|
|
self.rotation_config = {
|
|
'max_size': 1024 * 1024 * 100, # 100MB
|
|
'backup_count': 30
|
|
}
|
|
|
|
def start(self):
|
|
"""优化后的主循环"""
|
|
self.running = True
|
|
batch_buffer = []
|
|
|
|
while self.running:
|
|
try:
|
|
# 批量收集模式
|
|
while not self.logBuffer.empty() and len(batch_buffer) < self.batch_size:
|
|
data = self.logBuffer.get_nowait()
|
|
self._preprocess_data(data)
|
|
batch_buffer.append(data)
|
|
self.logMsgData.emit(data)
|
|
|
|
if batch_buffer:
|
|
self._process_batch(batch_buffer)
|
|
batch_buffer.clear()
|
|
|
|
QThread.msleep(10) # 降低CPU占用
|
|
|
|
except Exception as e:
|
|
self.errorOccurred.emit(f"日志处理异常: {str(e)}")
|
|
QThread.msleep(1000)
|
|
|
|
def _preprocess_data(self, data):
|
|
"""数据预处理"""
|
|
try:
|
|
# 统一时间格式
|
|
data["time"] = QDateTime.currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz")
|
|
# 保留原始换行符但转义显示
|
|
data["msg"] = data["msg"].replace("\n", "↵")
|
|
except KeyError as e:
|
|
self.errorOccurred.emit(f"缺失必要字段: {str(e)}")
|
|
|
|
def _process_batch(self, batch):
|
|
"""批量处理逻辑"""
|
|
try:
|
|
# 文件写入
|
|
self.mutex.lock()
|
|
self._batch_write(batch)
|
|
self.mutex.unlock()
|
|
|
|
except PermissionError as e:
|
|
self.errorOccurred.emit(f"文件权限错误: {str(e)}")
|
|
except IOError as e:
|
|
self._handle_io_error(e)
|
|
finally:
|
|
self.mutex.unlock()
|
|
|
|
def _batch_write(self, batch):
|
|
"""优化后的批量写入"""
|
|
log_path = projectManager.getLogPath()
|
|
if not log_path: return
|
|
|
|
# 创建日期文件句柄
|
|
main_file = os.path.join(log_path, f"log_{self.localDateTime}.txt")
|
|
|
|
# 写入主日志
|
|
self._write_to_file(main_file, batch)
|
|
|
|
# 处理标签日志
|
|
tag_files = {}
|
|
for entry in batch:
|
|
if tag := entry.get("tag"):
|
|
tag_file = os.path.join(log_path, f"log_{self.localDateTime}_{tag}.txt")
|
|
tag_files.setdefault(tag_file, []).append(entry)
|
|
|
|
# 批量写入标签日志
|
|
for file_path, entries in tag_files.items():
|
|
self._write_to_file(file_path, entries)
|
|
|
|
def _write_to_file(self, file_path, entries):
|
|
"""带轮转的写入操作"""
|
|
# 检查文件大小
|
|
if os.path.exists(file_path) and os.path.getsize(file_path) > self.rotation_config['max_size']:
|
|
self._rotate_file(file_path)
|
|
|
|
# 使用缓存的文件句柄
|
|
try:
|
|
with open(file_path, "a+", encoding='utf-8') as f:
|
|
for entry in entries:
|
|
json.dump({
|
|
"time": entry["time"],
|
|
"level": entry["level"],
|
|
"msg": entry["msg"],
|
|
"color": entry.get("color", "#000000"),
|
|
"tag": entry.get("tag", "")
|
|
}, f, ensure_ascii=False)
|
|
f.write(f'{self.endStr}\n')
|
|
except Exception as e:
|
|
self.errorOccurred.emit(f"文件写入失败: {file_path} - {str(e)}")
|
|
|
|
def _rotate_file(self, file_path):
|
|
"""日志轮转实现"""
|
|
base_name = os.path.basename(file_path).split('.')[0]
|
|
log_dir = os.path.dirname(file_path)
|
|
|
|
# 删除最旧日志
|
|
existing_logs = sorted([f for f in os.listdir(log_dir) if f.startswith(base_name)])
|
|
if len(existing_logs) >= self.rotation_config['backup_count']:
|
|
os.remove(os.path.join(log_dir, existing_logs[0]))
|
|
|
|
# 重命名当前日志
|
|
timestamp = QDateTime.currentDateTime().toString("yyyyMMddHHmmss")
|
|
new_name = f"{base_name}_{timestamp}.bak.txt"
|
|
os.rename(file_path, os.path.join(log_dir, new_name))
|
|
|
|
def _handle_io_error(self, error):
|
|
"""IO错误处理"""
|
|
if error.errno == 28: # No space left
|
|
self.errorOccurred.emit("磁盘空间不足,自动清除旧日志")
|
|
self._cleanup_old_logs()
|
|
else:
|
|
self.errorOccurred.emit(f"IO错误: {str(error)}")
|
|
|
|
def _cleanup_old_logs(self):
|
|
"""紧急空间清理"""
|
|
log_path = projectManager.getLogPath()
|
|
all_files = sorted(
|
|
[os.path.join(log_path, f) for f in os.listdir(log_path) if f.startswith("log_")],
|
|
key=os.path.getctime
|
|
)
|
|
while len(all_files) > 5:
|
|
os.remove(all_files.pop(0))
|
|
|
|
def appendLogFile(self, data):
|
|
"""线程安全的队列添加"""
|
|
try:
|
|
if self.logBuffer.qsize() < 1900: # 保留100缓冲空间
|
|
self.logBuffer.put_nowait(data)
|
|
else:
|
|
self.errorOccurred.emit("日志队列已满,丢弃新条目")
|
|
except queue.Full:
|
|
pass
|
|
|
|
def stop(self):
|
|
"""安全停止方法"""
|
|
self.running = False
|
|
# 处理剩余日志
|
|
while not self.logBuffer.empty():
|
|
QThread.msleep(100)
|
|
# 关闭所有文件句柄
|
|
self.file_handles.clear()
|
|
class LogManager(QObject):
|
|
logMsg = pyqtSignal(dict)
|
|
logMsgData = pyqtSignal(dict)
|
|
proChange = pyqtSignal()
|
|
def __init__(self, parent = None):
|
|
super().__init__()
|
|
self.thread = QThread()
|
|
self.localDateTime = QDateTime.currentDateTime().toString('yyyy-MM-dd_HH-mm-ss')
|
|
self.logWorker = LogWorker(self.localDateTime)
|
|
self.logWorker.logMsgData.connect(self.onLogMsgData)
|
|
self.logWorker.moveToThread(self.thread)
|
|
self.thread.started.connect(self.logWorker.start)
|
|
self.thread.start()
|
|
|
|
|
|
def addLogMsg(self, data):
|
|
try:
|
|
self.logWorker.logBuffer.put(data)
|
|
except Exception as e:
|
|
log.error("addLogMsg Error",e)
|
|
|
|
def onLogMsgData(self, msgData):
|
|
self.logMsgData.emit(msgData)
|
|
|
|
def getInfo(self, proId):
|
|
logPath = projectManager.getLogPath()
|
|
logFilePath = os.path.join(logPath, f"log{self.localDateTime}.txt")
|
|
return logFilePath
|
|
|
|
def loadProLog(self, proId):
|
|
logPath = projectManager.getLogPath()
|
|
logFilePath = os.path.join(logPath, f"log{self.localDateTime}.txt")
|
|
return logFilePath
|
|
|
|
|
|
logManager = LogManager()
|
|
|
|
|