151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
from channels.db import database_sync_to_async
|
|||
|
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
|||
|
|
from django.conf import settings
|
|||
|
|
|
|||
|
|
from apps.core.models import AiAlgorithm, AiAlgorithmClass, AiAlgorithmTrainRecords
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TrainLogConsumer(AsyncWebsocketConsumer):
|
|||
|
|
async def connect(self):
|
|||
|
|
self.train_id = self.scope["url_route"]["kwargs"]["train_id"]
|
|||
|
|
self.group_name = f"train_{self.train_id}"
|
|||
|
|
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
|||
|
|
print(f"客户端[{self.train_id}],websocket连接服务器成功!")
|
|||
|
|
await self.accept()
|
|||
|
|
self._stop_event = asyncio.Event()
|
|||
|
|
self._tail_task = asyncio.create_task(self._tail_train_log())
|
|||
|
|
|
|||
|
|
async def disconnect(self, close_code):
|
|||
|
|
if hasattr(self, "_stop_event"):
|
|||
|
|
self._stop_event.set()
|
|||
|
|
if hasattr(self, "_tail_task"):
|
|||
|
|
self._tail_task.cancel()
|
|||
|
|
try:
|
|||
|
|
await self._tail_task
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
|||
|
|
|
|||
|
|
async def receive(self, text_data):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
async def send_log(self, event):
|
|||
|
|
message = event["message"]
|
|||
|
|
await self.send(text_data=message)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _is_valid_json(text: str) -> bool:
|
|||
|
|
try:
|
|||
|
|
json.loads(text)
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
async def _send_json(self, payload):
|
|||
|
|
await self.send(text_data=json.dumps(payload, ensure_ascii=False))
|
|||
|
|
|
|||
|
|
@database_sync_to_async
|
|||
|
|
def _resolve_log_file_path(self) -> str:
|
|||
|
|
ai_train = AiAlgorithmTrainRecords.objects.get(id=self.train_id)
|
|||
|
|
ai_algorithm = AiAlgorithm.objects.get(id=ai_train.algorithm_id)
|
|||
|
|
ai_algorithm_class = AiAlgorithmClass.objects.get(id=ai_algorithm.algorithm_class)
|
|||
|
|
run_name = "train"
|
|||
|
|
flat_train_path = os.path.join(settings.FILESPACE_ROOT_PATH, ai_train.train_code)
|
|||
|
|
nested_train_path = os.path.join(
|
|||
|
|
settings.FILESPACE_ROOT_PATH,
|
|||
|
|
ai_algorithm_class.classcode,
|
|||
|
|
ai_algorithm.algorithm_code,
|
|||
|
|
"trains",
|
|||
|
|
ai_train.train_code,
|
|||
|
|
)
|
|||
|
|
if getattr(ai_algorithm, "algorithm_engine", None) == "02":
|
|||
|
|
candidates = [
|
|||
|
|
os.path.join(nested_train_path, "output", f"{run_name}.log"),
|
|||
|
|
os.path.join(flat_train_path, "output", f"{run_name}.log"),
|
|||
|
|
]
|
|||
|
|
elif getattr(ai_algorithm, "algorithm_engine", None) in {"03", "04"}:
|
|||
|
|
candidates = [
|
|||
|
|
os.path.join(nested_train_path, "runs", f"{run_name}.log"),
|
|||
|
|
os.path.join(flat_train_path, "runs", f"{run_name}.log"),
|
|||
|
|
]
|
|||
|
|
else:
|
|||
|
|
candidates = [
|
|||
|
|
os.path.join(flat_train_path, "runs", f"{run_name}.log"),
|
|||
|
|
os.path.join(nested_train_path, "runs", f"{run_name}.log"),
|
|||
|
|
]
|
|||
|
|
for path in candidates:
|
|||
|
|
if os.path.exists(path):
|
|||
|
|
return path
|
|||
|
|
return candidates[0]
|
|||
|
|
|
|||
|
|
async def _emit_log_lines(self, payload: bytes) -> None:
|
|||
|
|
for raw_line in payload.splitlines(keepends=False):
|
|||
|
|
line = raw_line.decode("utf-8", errors="ignore").strip()
|
|||
|
|
if not line:
|
|||
|
|
continue
|
|||
|
|
if self._is_valid_json(line):
|
|||
|
|
await self.send(text_data=line)
|
|||
|
|
else:
|
|||
|
|
await self._send_json({"type": "waiting", "text": line})
|
|||
|
|
|
|||
|
|
async def _tail_train_log(self) -> None:
|
|||
|
|
try:
|
|||
|
|
log_file_path = await self._resolve_log_file_path()
|
|||
|
|
except Exception as e:
|
|||
|
|
await self._send_json({"type": "error", "text": f"无法定位训练日志文件:{str(e)}"})
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
last_pos = 0
|
|||
|
|
buf = b""
|
|||
|
|
while not self._stop_event.is_set():
|
|||
|
|
if not os.path.exists(log_file_path):
|
|||
|
|
last_pos = 0
|
|||
|
|
buf = b""
|
|||
|
|
await asyncio.sleep(0.5)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
current_size = os.path.getsize(log_file_path)
|
|||
|
|
if current_size < last_pos:
|
|||
|
|
last_pos = 0
|
|||
|
|
buf = b""
|
|||
|
|
if current_size == last_pos:
|
|||
|
|
await asyncio.sleep(0.2)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
with open(log_file_path, "rb") as f:
|
|||
|
|
f.seek(last_pos)
|
|||
|
|
chunk = f.read()
|
|||
|
|
last_pos = f.tell()
|
|||
|
|
|
|||
|
|
if not chunk:
|
|||
|
|
await asyncio.sleep(0.2)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
buf += chunk
|
|||
|
|
normalized = buf.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|||
|
|
if normalized.endswith(b"\n"):
|
|||
|
|
completed = normalized
|
|||
|
|
buf = b""
|
|||
|
|
else:
|
|||
|
|
split_idx = normalized.rfind(b"\n")
|
|||
|
|
if split_idx == -1:
|
|||
|
|
buf = normalized
|
|||
|
|
await asyncio.sleep(0.2)
|
|||
|
|
continue
|
|||
|
|
completed = normalized[: split_idx + 1]
|
|||
|
|
buf = normalized[split_idx + 1 :]
|
|||
|
|
|
|||
|
|
await self._emit_log_lines(completed)
|
|||
|
|
await asyncio.sleep(0.05)
|
|||
|
|
except asyncio.CancelledError:
|
|||
|
|
return
|
|||
|
|
except Exception as e:
|
|||
|
|
await self._send_json({"type": "error", "text": f"训练日志读取失败:{str(e)}"})
|
|||
|
|
await asyncio.sleep(1.0)
|
|||
|
|
|