""" RotatingFileHandler that detects external deletion or rotation. Stdlib ``RotatingFileHandler`` holds an open file descriptor for the lifetime of the handler. If the target file is deleted (``rm``) or rotated out (``logrotate`` without ``copytruncate``), the handler keeps writing to the now-orphaned inode until its own size-based rotation finally triggers — silently losing every line in between. Stdlib ``WatchedFileHandler`` solves exactly this problem but doesn't rotate by size. This subclass combines both: before each emit we stat the configured path and compare its inode/device to the currently open file; on mismatch we close and reopen. Cheap: one ``os.stat`` per log record. Matches the pattern used by ``decnet/collector/worker.py:_reopen_if_needed``. """ from __future__ import annotations import logging import logging.handlers import os class InodeAwareRotatingFileHandler(logging.handlers.RotatingFileHandler): """RotatingFileHandler that reopens the target on external rotation/deletion.""" def _should_reopen(self) -> bool: if self.stream is None: return True try: disk_stat = os.stat(self.baseFilename) except FileNotFoundError: return True except OSError: return False try: open_stat = os.fstat(self.stream.fileno()) except OSError: return True return (disk_stat.st_ino != open_stat.st_ino or disk_stat.st_dev != open_stat.st_dev) def emit(self, record: logging.LogRecord) -> None: if self._should_reopen(): try: if self.stream is not None: self.close() except Exception: # nosec B110 pass try: self.stream = self._open() except OSError: # A logging handler MUST NOT crash its caller. If we can't # reopen (e.g. file is root-owned after `sudo decnet deploy` # and the current process is non-root), defer to the stdlib # error path, which just prints a traceback to stderr. self.handleError(record) return super().emit(record)