some fixes
This commit is contained in:
@@ -7,9 +7,12 @@ state shared across them (runtime services, readers, history buffers, timer).
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -59,6 +62,7 @@ class AppWindow(
|
||||
super().__init__()
|
||||
|
||||
self._init_paths(project_root)
|
||||
self._init_headless_logger()
|
||||
self._init_runtime_services()
|
||||
self._init_config_profile_state()
|
||||
self._init_reader_handles()
|
||||
@@ -76,6 +80,42 @@ class AppWindow(
|
||||
self._root_profile_path = project_root / "run_config.json"
|
||||
self._active_profile_path = self._root_profile_path
|
||||
self._pending_startup_log_entries: list[tuple[str, str, str | None]] = []
|
||||
# Guards closeEvent against re-entrant teardown (e.g. a second signal).
|
||||
self._closing = False
|
||||
|
||||
def _init_headless_logger(self) -> None:
|
||||
"""Create a Python logger so headless WARN/ERROR reach journald and disk.
|
||||
|
||||
In headless mode the in-app log only reaches an offscreen widget, so an
|
||||
operator (or `journalctl`) would never see failures. We attach a stderr
|
||||
StreamHandler (captured by journald) plus a small rotating file under
|
||||
`runtime/logs`; in GUI mode no handler is attached and the logger stays
|
||||
inert, preserving the visible log widget as the sole sink.
|
||||
"""
|
||||
self._headless_logger: logging.Logger | None = None
|
||||
if not self._is_truthy_env("RADAR_SYSTEM_HEADLESS"):
|
||||
return
|
||||
logger = logging.getLogger("radar_system.gui")
|
||||
logger.setLevel(logging.WARNING)
|
||||
logger.propagate = False
|
||||
logger.handlers.clear()
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(levelname)-5s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
stream_handler = logging.StreamHandler(stream=sys.stderr)
|
||||
stream_handler.setFormatter(formatter)
|
||||
logger.addHandler(stream_handler)
|
||||
# A rotating file keeps recent failures around after a journald restart.
|
||||
with suppress(Exception):
|
||||
log_dir = self._project_root / "python_app/runtime/logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_handler = RotatingFileHandler(
|
||||
log_dir / "gui.log", maxBytes=1_000_000, backupCount=3, encoding="utf-8"
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
self._headless_logger = logger
|
||||
|
||||
def _init_runtime_services(self) -> None:
|
||||
"""Initialize long-lived service objects used by mixins."""
|
||||
@@ -461,6 +501,15 @@ class AppWindow(
|
||||
if level_upper == "ERROR" and hasattr(self, "_status_label"):
|
||||
self._status_label.setText("Status: error")
|
||||
|
||||
# In headless mode the offscreen widget above is invisible, so also mirror
|
||||
# WARN/ERROR to the Python logger (stderr -> journald, plus rotating file)
|
||||
# where an operator can actually observe failures.
|
||||
headless_logger = getattr(self, "_headless_logger", None)
|
||||
if headless_logger is not None and level_upper in {"WARN", "ERROR"}:
|
||||
log_message = text if not details else f"{text}\n{details}"
|
||||
log_level = logging.ERROR if level_upper == "ERROR" else logging.WARNING
|
||||
headless_logger.log(log_level, log_message)
|
||||
|
||||
def _log(self, text: str, *, once_key: str | None = None) -> None:
|
||||
"""Append informational message to runtime log panel."""
|
||||
self._append_log_entry("INFO", text, once_key=once_key)
|
||||
@@ -564,6 +613,12 @@ class AppWindow(
|
||||
|
||||
def closeEvent(self, event) -> None: # noqa: N802
|
||||
"""Ensure workers and dialogs are closed before window destruction."""
|
||||
if self._closing:
|
||||
# Re-entrant close (second signal, or window.close() after the event loop
|
||||
# already returned): teardown is in progress or done — do nothing more.
|
||||
super().closeEvent(event)
|
||||
return
|
||||
self._closing = True
|
||||
try:
|
||||
# 0) Stop the GPIO button watcher so a late press cannot start work.
|
||||
self._stop_control_button_watcher()
|
||||
|
||||
Reference in New Issue
Block a user