"""WebSocket 日志流""" import asyncio import json from datetime import datetime from pathlib import Path from typing import Dict, AsyncGenerator from ..config import LOG_QUEUE_MAX_SIZE class LogStreamer: """日志流管理器""" def __init__(self): self._queues: Dict[str, asyncio.Queue] = {} self._subscribers: Dict[str, list] = {} self._log_lines: Dict[str, list] = {} self._completed: set[str] = set() def create_queue(self, task_id: str) -> asyncio.Queue: """创建任务的日志队列(重复调用时保留已有日志)""" if task_id in self._queues: return self._queues[task_id] queue = asyncio.Queue(maxsize=LOG_QUEUE_MAX_SIZE) self._queues[task_id] = queue self._log_lines.setdefault(task_id, []) self._completed.discard(task_id) return queue async def emit(self, task_id: str, message: str, level: str = "info"): """发送日志消息""" log_entry = { "timestamp": datetime.now().isoformat(), "level": level, "message": message, } # 收集日志 if task_id in self._log_lines: self._log_lines[task_id].append(log_entry) if task_id not in self._queues: return try: self._queues[task_id].put_nowait(log_entry) except asyncio.QueueFull: # 队列满了,丢弃最旧的消息 try: self._queues[task_id].get_nowait() self._queues[task_id].put_nowait(log_entry) except asyncio.QueueEmpty: pass async def emit_step(self, task_id: str, step: str): """发送步骤标记""" await self.emit(task_id, f"[{step}]", level="step") async def emit_error(self, task_id: str, error: str): """发送错误消息""" await self.emit(task_id, error, level="error") async def emit_warning(self, task_id: str, warning: str): """发送警告消息""" await self.emit(task_id, warning, level="warn") async def subscribe(self, task_id: str) -> AsyncGenerator[dict, None]: """订阅任务日志""" queue = self._queues.get(task_id) if not queue: return # Messages are stored both in history and the live queue. Replay the # history once, then discard its queue copies before waiting for new logs. existing = list(self._log_lines.get(task_id, [])) completed = task_id in self._completed while not queue.empty(): queue.get_nowait() for entry in existing: yield entry if completed: return # 流式推送新消息 while True: try: msg = await asyncio.wait_for(queue.get(), timeout=30) if msg is None: # 结束信号 break yield msg except asyncio.TimeoutError: # 发送心跳 yield {"timestamp": datetime.now().isoformat(), "level": "heartbeat", "message": ""} except Exception: break def complete(self, task_id: str): """标记任务日志结束""" self._completed.add(task_id) if task_id in self._queues: try: self._queues[task_id].put_nowait(None) except asyncio.QueueFull: pass def cleanup(self, task_id: str): """清理任务日志队列""" self._queues.pop(task_id, None) self._completed.discard(task_id) def save_log(self, task_id: str, build_dir: Path): """将收集的日志保存到独立日志目录(不会随构建目录删除)""" lines = self._log_lines.get(task_id) if not lines: return # 保存到 backend/logs/ 目录 logs_dir = Path(__file__).parent.parent / "logs" logs_dir.mkdir(exist_ok=True) log_path = logs_dir / f"{task_id}.log" with open(log_path, "w", encoding="utf-8") as f: for entry in lines: ts = entry["timestamp"] level = entry["level"].upper() msg = entry["message"] f.write(f"[{ts}] [{level}] {msg}\n") # 同时保存到构建目录(如果存在) if build_dir and build_dir.exists(): with open(build_dir / "build.log", "w", encoding="utf-8") as f: for entry in lines: ts = entry["timestamp"] level = entry["level"].upper() msg = entry["message"] f.write(f"[{ts}] [{level}] {msg}\n") def get_log(self, build_dir: Path, task_id: str = None) -> str: """读取已保存的打包日志(优先从独立日志目录读取)""" # 优先从独立日志目录读取 if task_id: logs_dir = Path(__file__).parent.parent / "logs" log_path = logs_dir / f"{task_id}.log" if log_path.exists(): return log_path.read_text(encoding="utf-8") # 回退到构建目录 if build_dir: log_path = build_dir / "build.log" if log_path.exists(): return log_path.read_text(encoding="utf-8") return "" # 全局单例 log_streamer = LogStreamer()