| 408 | |
| 409 | |
| 410 | class LogSession: |
| 411 | def __init__(self, path: Path, socketio: SocketIO, shutdown_event: threading.Event): |
| 412 | self.path = path |
| 413 | self.socketio = socketio |
| 414 | self.shutdown_event = shutdown_event |
| 415 | self.room = _room_for_path(path) |
| 416 | self.parser = LogParser() |
| 417 | self.lock = threading.Lock() |
| 418 | self.subscribers: Set[str] = set() |
| 419 | self.missing = False |
| 420 | self._watching = False |
| 421 | self._thread: Optional[threading.Thread] = None |
| 422 | self._last_position = 0 |
| 423 | self._last_size = 0 |
| 424 | self._last_inode: Optional[int] = None |
| 425 | self._loaded = False |
| 426 | |
| 427 | def _reset_parser(self) -> None: |
| 428 | with self.lock: |
| 429 | self.parser = LogParser() |
| 430 | self._last_position = 0 |
| 431 | self._last_size = 0 |
| 432 | |
| 433 | def _read_full(self) -> None: |
| 434 | with open(self.path, "r", encoding="utf-8", errors="replace") as f: |
| 435 | for line in f: |
| 436 | self.parser.parse_line(line.rstrip("\n")) |
| 437 | self._last_position = f.tell() |
| 438 | try: |
| 439 | stat = self.path.stat() |
| 440 | self._last_size = stat.st_size |
| 441 | self._last_inode = stat.st_ino |
| 442 | except Exception: |
| 443 | pass |
| 444 | |
| 445 | def load_initial(self) -> None: |
| 446 | if not self.path.exists(): |
| 447 | self.missing = True |
| 448 | return |
| 449 | self.missing = False |
| 450 | self._reset_parser() |
| 451 | with self.lock: |
| 452 | self._read_full() |
| 453 | self._loaded = True |
| 454 | |
| 455 | def start(self) -> None: |
| 456 | if self._thread and self._thread.is_alive(): |
| 457 | return |
| 458 | self._watching = True |
| 459 | self._thread = threading.Thread(target=self._watch_loop, daemon=True) |
| 460 | self._thread.start() |
| 461 | |
| 462 | def stop(self) -> None: |
| 463 | self._watching = False |
| 464 | |
| 465 | def state(self) -> Dict[str, Any]: |
| 466 | with self.lock: |
| 467 | state = self.parser.to_dict() |