"""Runtime helpers for console-less Windows launches.""" from __future__ import annotations import sys import threading import traceback from datetime import datetime from pathlib import Path import tempfile from typing import Callable, TypeVar from version_info import module_version __version__ = module_version(__name__) BASE_DIR = Path(__file__).resolve().parent FATAL_LOG = BASE_DIR / "warehouse_fatal.log" TEMP_DIR = Path(tempfile.gettempdir()) _STDIO_HANDLES = [] _EXCEPTION_LOGGING_CONFIGURED: set[str] = set() T = TypeVar("T") def _open_log_file(path: Path): """Open a log path, falling back to the user temp directory when needed.""" try: path.parent.mkdir(parents=True, exist_ok=True) return path.open("a", encoding="utf-8", buffering=1) except Exception: fallback = TEMP_DIR / path.name fallback.parent.mkdir(parents=True, exist_ok=True) return fallback.open("a", encoding="utf-8", buffering=1) def ensure_stdio(app_name: str) -> None: """Give ``pythonw`` a real stdout/stderr target before loggers are imported.""" stamp = datetime.now().strftime("%Y%m%d") if sys.stdout is None: handle = _open_log_file(BASE_DIR / f"{app_name}_stdout_{stamp}.log") _STDIO_HANDLES.append(handle) sys.stdout = handle if sys.stderr is None: handle = _open_log_file(BASE_DIR / f"{app_name}_stderr_{stamp}.log") _STDIO_HANDLES.append(handle) sys.stderr = handle def log_fatal(app_name: str, exc: BaseException) -> None: """Write one startup/runtime crash to a persistent log file.""" with _open_log_file(FATAL_LOG) as handle: handle.write("\n" + "=" * 80 + "\n") handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}\n") handle.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))) def log_exception(app_name: str, exc: BaseException, *, context: str = "") -> None: """Write a handled or callback exception to the persistent fatal log.""" with _open_log_file(FATAL_LOG) as handle: handle.write("\n" + "=" * 80 + "\n") handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {app_name}") if context: handle.write(f" | {context}") handle.write("\n") handle.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))) def configure_exception_logging(app_name: str, root=None, loop=None) -> None: """Install process, Tk and asyncio exception hooks for console-less apps.""" if app_name in _EXCEPTION_LOGGING_CONFIGURED: return _EXCEPTION_LOGGING_CONFIGURED.add(app_name) previous_excepthook = sys.excepthook def _sys_excepthook(exc_type, exc, tb): log_exception(app_name, exc, context="sys.excepthook") if previous_excepthook: previous_excepthook(exc_type, exc, tb) sys.excepthook = _sys_excepthook if hasattr(threading, "excepthook"): previous_threading_excepthook = threading.excepthook def _threading_excepthook(args): log_exception(app_name, args.exc_value, context=f"thread={getattr(args.thread, 'name', '')}") previous_threading_excepthook(args) threading.excepthook = _threading_excepthook if root is not None: def _tk_exception(exc_type, exc, tb): log_exception(app_name, exc, context="tkinter callback") try: import tkinter.messagebox as messagebox messagebox.showerror( app_name, "Errore applicativo registrato nei log.\n\n" "Se l'operazione era in corso, ripeterla o avvisare il responsabile.", parent=root, ) except Exception: pass try: root.report_callback_exception = _tk_exception except Exception: pass if loop is not None: previous_loop_handler = None try: previous_loop_handler = loop.get_exception_handler() except Exception: previous_loop_handler = None def _loop_exception_handler(active_loop, context): exc = context.get("exception") if exc is not None: log_exception(app_name, exc, context=f"asyncio: {context.get('message', '')}") else: log_runtime_event(app_name, f"ASYNCIO {context!r}") if previous_loop_handler is not None: previous_loop_handler(active_loop, context) try: loop.set_exception_handler(_loop_exception_handler) except Exception: pass def log_runtime_event(app_name: str, message: str) -> None: """Write a lightweight launch/shutdown trace for console-less starts.""" safe_name = "".join(ch if ch.isalnum() or ch in ("_", "-") else "_" for ch in app_name.lower()) path = BASE_DIR / f"{safe_name}_launch.log" with _open_log_file(path) as handle: handle.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} | {message}\n") def show_fatal_message(app_name: str, exc: BaseException) -> None: """Show a best-effort message box for console-less launches.""" try: import tkinter as tk from tkinter import messagebox root = tk.Tk() root.withdraw() messagebox.showerror( app_name, "Avvio non riuscito.\n\n" f"Controlla i log in:\n{BASE_DIR}\n\n" f"Se non ci sono, controlla anche:\n{TEMP_DIR}\n\n" f"{exc}", parent=root, ) root.destroy() except Exception: pass def run_with_fatal_log(app_name: str, func: Callable[[], T]) -> T: """Run an app entry point and persist otherwise invisible ``pythonw`` crashes.""" try: log_runtime_event( app_name, f"START exe={sys.executable!r} version={sys.version.split()[0]} cwd={Path.cwd()} base={BASE_DIR} argv={sys.argv!r}", ) result = func() log_runtime_event(app_name, f"RETURN result={result!r}") return result except BaseException as exc: log_runtime_event(app_name, f"EXCEPTION type={type(exc).__name__} value={exc!r}") log_fatal(app_name, exc) show_fatal_message(app_name, exc) raise