diff --git a/src/main.py b/src/main.py index 47a5b8e..d6c16ce 100644 --- a/src/main.py +++ b/src/main.py @@ -9,6 +9,7 @@ if vendor_dir.exists(): import os import logging +from logging.handlers import QueueHandler, QueueListener import signal import time import argparse @@ -38,6 +39,7 @@ from utils import ( ) from utils.clipboard import ClipboardUnavailableError from utils.atomic_write import atomic_write +from utils.logging_utils import ConsolePauseFilter import queue from local_bip85.bip85 import Bip85Error @@ -77,39 +79,43 @@ def load_global_config() -> dict: return {} -def configure_logging(): - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) # Keep this as DEBUG to capture all logs +_queue_listener: QueueListener | None = None + + +def configure_logging(): + """Configure application-wide logging with queue-based handlers.""" + global _queue_listener + + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) - # Remove all handlers associated with the root logger object for handler in logger.handlers[:]: logger.removeHandler(handler) - # Ensure the 'logs' directory exists log_directory = Path("logs") - if not log_directory.exists(): - log_directory.mkdir(parents=True, exist_ok=True) + log_directory.mkdir(parents=True, exist_ok=True) - # Create handlers - c_handler = logging.StreamHandler(sys.stdout) - f_handler = logging.FileHandler(log_directory / "main.log") + log_queue: queue.Queue[logging.LogRecord] = queue.Queue() + queue_handler = QueueHandler(log_queue) - # Set levels: only errors and critical messages will be shown in the console - c_handler.setLevel(logging.ERROR) - f_handler.setLevel(logging.DEBUG) + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(logging.ERROR) + console_handler.addFilter(ConsolePauseFilter()) + + file_handler = logging.FileHandler(log_directory / "main.log") + file_handler.setLevel(logging.DEBUG) - # Create formatters and add them to handlers formatter = logging.Formatter( - "%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]" + "%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]", ) - c_handler.setFormatter(formatter) - f_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + file_handler.setFormatter(formatter) - # Add handlers to the logger - logger.addHandler(c_handler) - logger.addHandler(f_handler) + _queue_listener = QueueListener(log_queue, console_handler, file_handler) + _queue_listener.start() + + logger.addHandler(queue_handler) - # Set logging level for third-party libraries to WARNING to suppress their debug logs logging.getLogger("monstr").setLevel(logging.WARNING) logging.getLogger("nostr").setLevel(logging.WARNING) diff --git a/src/utils/logging_utils.py b/src/utils/logging_utils.py new file mode 100644 index 0000000..f11c07f --- /dev/null +++ b/src/utils/logging_utils.py @@ -0,0 +1,24 @@ +import logging + +_console_paused = False + + +class ConsolePauseFilter(logging.Filter): + """Filter that blocks records when console logging is paused.""" + + def filter( + self, record: logging.LogRecord + ) -> bool: # pragma: no cover - small utility + return not _console_paused + + +def pause_console_logging() -> None: + """Temporarily pause logging to console handlers.""" + global _console_paused + _console_paused = True + + +def resume_console_logging() -> None: + """Resume logging to console handlers.""" + global _console_paused + _console_paused = False diff --git a/src/utils/seed_prompt.py b/src/utils/seed_prompt.py index 7511488..45759e1 100644 --- a/src/utils/seed_prompt.py +++ b/src/utils/seed_prompt.py @@ -15,6 +15,7 @@ except ImportError: # pragma: no cover - POSIX only tty = None # type: ignore from utils.terminal_utils import clear_screen +from utils.logging_utils import pause_console_logging, resume_console_logging DEFAULT_MAX_ATTEMPTS = 5 @@ -107,12 +108,15 @@ def _masked_input_posix(prompt: str) -> str: def masked_input(prompt: str) -> str: """Return input from the user while masking typed characters.""" func = _masked_input_windows if sys.platform == "win32" else _masked_input_posix + pause_console_logging() try: return func(prompt) except KeyboardInterrupt: raise except Exception: # pragma: no cover - fallback when TTY operations fail return input(prompt) + finally: + resume_console_logging() def prompt_seed_words(count: int = 12, *, max_attempts: int | None = None) -> str: