2026-03-02 14:29:58 +08:00
|
|
|
#!/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
|
|
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
class LogWorker(QObject):
|
2026-04-17 15:31:41 +08:00
|
|
|
logMsg = pyqtSignal(dict)
|
|
|
|
|
logMsgData = pyqtSignal(dict)
|
|
|
|
|
errorOccurred = pyqtSignal(str)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
def __init__(self, localDateTime=None):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.running = False
|
2026-04-17 15:31:41 +08:00
|
|
|
self.logBuffer = queue.Queue(maxsize=2000)
|
2026-03-02 14:29:58 +08:00
|
|
|
self.localDateTime = localDateTime
|
|
|
|
|
self.endStr = "---END"
|
2026-04-17 15:31:41 +08:00
|
|
|
self.mutex = QMutex()
|
|
|
|
|
self.batch_size = 50
|
2026-03-02 14:29:58 +08:00
|
|
|
self._init_file_rotation()
|
|
|
|
|
|
|
|
|
|
def _init_file_rotation(self):
|
|
|
|
|
"""日志轮转配置"""
|
|
|
|
|
self.rotation_config = {
|
|
|
|
|
'max_size': 1024 * 1024 * 100, # 100MB
|
|
|
|
|
'backup_count': 30
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def start(self):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""主循环"""
|
2026-03-02 14:29:58 +08:00
|
|
|
self.running = True
|
|
|
|
|
batch_buffer = []
|
2026-04-17 15:31:41 +08:00
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
while self.running:
|
|
|
|
|
try:
|
2026-04-17 15:31:41 +08:00
|
|
|
# 批量收集
|
2026-03-02 14:29:58 +08:00
|
|
|
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)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
if batch_buffer:
|
|
|
|
|
self._process_batch(batch_buffer)
|
|
|
|
|
batch_buffer.clear()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
QThread.msleep(10)
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
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")
|
2026-04-17 15:31:41 +08:00
|
|
|
data["msg"] = data.get("msg", "").replace("\n", "↵")
|
2026-03-02 14:29:58 +08:00
|
|
|
except KeyError as e:
|
|
|
|
|
self.errorOccurred.emit(f"缺失必要字段: {str(e)}")
|
|
|
|
|
|
|
|
|
|
def _process_batch(self, batch):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""批量处理"""
|
|
|
|
|
locked = False
|
|
|
|
|
try:
|
2026-03-02 14:29:58 +08:00
|
|
|
self.mutex.lock()
|
2026-04-17 15:31:41 +08:00
|
|
|
locked = True
|
2026-03-02 14:29:58 +08:00
|
|
|
self._batch_write(batch)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
self.errorOccurred.emit(f"文件权限错误: {str(e)}")
|
|
|
|
|
except IOError as e:
|
|
|
|
|
self._handle_io_error(e)
|
2026-04-17 15:31:41 +08:00
|
|
|
except Exception as e:
|
|
|
|
|
self.errorOccurred.emit(f"批量写入异常: {str(e)}")
|
2026-03-02 14:29:58 +08:00
|
|
|
finally:
|
2026-04-17 15:31:41 +08:00
|
|
|
if locked:
|
|
|
|
|
self.mutex.unlock()
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
def _batch_write(self, batch):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""批量写入"""
|
2026-03-02 14:29:58 +08:00
|
|
|
log_path = projectManager.getLogPath()
|
2026-04-17 15:31:41 +08:00
|
|
|
if not log_path:
|
|
|
|
|
return
|
2026-03-02 14:29:58 +08:00
|
|
|
|
2026-04-17 15:31:41 +08:00
|
|
|
# 主日志文件
|
|
|
|
|
main_file = os.path.join(log_path, f"log{self.localDateTime}.txt")
|
2026-03-02 14:29:58 +08:00
|
|
|
self._write_to_file(main_file, batch)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
# 标签日志
|
2026-03-02 14:29:58 +08:00
|
|
|
tag_files = {}
|
|
|
|
|
for entry in batch:
|
2026-04-17 15:31:41 +08:00
|
|
|
tag = entry.get("tag", "")
|
|
|
|
|
if tag:
|
|
|
|
|
tag_file = os.path.join(log_path, f"log{self.localDateTime}_{tag}.txt")
|
2026-03-02 14:29:58 +08:00
|
|
|
tag_files.setdefault(tag_file, []).append(entry)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
for file_path, entries in tag_files.items():
|
|
|
|
|
self._write_to_file(file_path, entries)
|
|
|
|
|
|
|
|
|
|
def _write_to_file(self, file_path, entries):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""带轮转的写入"""
|
2026-03-02 14:29:58 +08:00
|
|
|
if os.path.exists(file_path) and os.path.getsize(file_path) > self.rotation_config['max_size']:
|
|
|
|
|
self._rotate_file(file_path)
|
2026-04-17 15:31:41 +08:00
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
try:
|
|
|
|
|
with open(file_path, "a+", encoding='utf-8') as f:
|
|
|
|
|
for entry in entries:
|
|
|
|
|
json.dump({
|
2026-04-17 15:31:41 +08:00
|
|
|
"time": entry.get("time", ""),
|
|
|
|
|
"level": entry.get("level", "DEBUG"),
|
|
|
|
|
"msg": entry.get("msg", ""),
|
2026-03-02 14:29:58 +08:00
|
|
|
"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):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""日志轮转"""
|
|
|
|
|
try:
|
|
|
|
|
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))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.errorOccurred.emit(f"日志轮转失败: {str(e)}")
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
def _handle_io_error(self, error):
|
|
|
|
|
"""IO错误处理"""
|
2026-04-17 15:31:41 +08:00
|
|
|
if hasattr(error, 'errno') and error.errno == 28:
|
2026-03-02 14:29:58 +08:00
|
|
|
self.errorOccurred.emit("磁盘空间不足,自动清除旧日志")
|
|
|
|
|
self._cleanup_old_logs()
|
|
|
|
|
else:
|
|
|
|
|
self.errorOccurred.emit(f"IO错误: {str(error)}")
|
|
|
|
|
|
|
|
|
|
def _cleanup_old_logs(self):
|
|
|
|
|
"""紧急空间清理"""
|
2026-04-17 15:31:41 +08:00
|
|
|
try:
|
|
|
|
|
log_path = projectManager.getLogPath()
|
|
|
|
|
if not log_path:
|
|
|
|
|
return
|
|
|
|
|
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))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.errorOccurred.emit(f"清理旧日志失败: {str(e)}")
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
def appendLogFile(self, data):
|
|
|
|
|
"""线程安全的队列添加"""
|
|
|
|
|
try:
|
2026-04-17 15:31:41 +08:00
|
|
|
self.logBuffer.put_nowait(data)
|
2026-03-02 14:29:58 +08:00
|
|
|
except queue.Full:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def stop(self):
|
2026-04-17 15:31:41 +08:00
|
|
|
"""安全停止"""
|
2026-03-02 14:29:58 +08:00
|
|
|
self.running = False
|
2026-04-17 15:31:41 +08:00
|
|
|
# 等待剩余日志处理完
|
|
|
|
|
wait_count = 0
|
|
|
|
|
while not self.logBuffer.empty() and wait_count < 50:
|
2026-03-02 14:29:58 +08:00
|
|
|
QThread.msleep(100)
|
2026-04-17 15:31:41 +08:00
|
|
|
wait_count += 1
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 14:29:58 +08:00
|
|
|
class LogManager(QObject):
|
|
|
|
|
logMsg = pyqtSignal(dict)
|
|
|
|
|
logMsgData = pyqtSignal(dict)
|
|
|
|
|
proChange = pyqtSignal()
|
2026-04-17 15:31:41 +08:00
|
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
2026-03-02 14:29:58 +08:00
|
|
|
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:
|
2026-04-17 15:31:41 +08:00
|
|
|
log.error(f"addLogMsg Error: {e}")
|
2026-03-02 14:29:58 +08:00
|
|
|
|
|
|
|
|
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()
|