1+ import faulthandler
12import logging
23import platform
34import sys
45import threading
56from logging .handlers import RotatingFileHandler
67from pathlib import Path
8+ from types import TracebackType
79
810from PySide6 .QtCore import (
911 QMessageLogContext ,
1719
1820_configured = False
1921_log_file_path : Path | None = None
22+ _crash_log_file = None
23+
24+
25+ def _safe_exc_info (
26+ exc_type : type [BaseException ],
27+ exc_value : BaseException | None ,
28+ exc_traceback : TracebackType | None ,
29+ ) -> (
30+ tuple [type [BaseException ], BaseException , TracebackType | None ]
31+ | tuple [None , None , None ]
32+ ):
33+ """Normalize exception tuple for logging APIs that require non-optional exception values."""
34+ if exc_value is None :
35+ return (None , None , None )
36+ return (exc_type , exc_value , exc_traceback )
37+
38+
39+ def _install_exception_hooks () -> None :
40+ def exc_hook (
41+ exc_type : type [BaseException ], exc : BaseException , tb : TracebackType | None
42+ ) -> None :
43+ logging .getLogger ("uncaught" ).critical (
44+ "Uncaught exception" ,
45+ exc_info = (exc_type , exc , tb ),
46+ )
47+
48+ sys .excepthook = exc_hook
49+
50+ def thread_exc_hook (args : threading .ExceptHookArgs ) -> None :
51+ logging .getLogger ("uncaught" ).critical (
52+ "Uncaught thread exception" ,
53+ exc_info = _safe_exc_info (args .exc_type , args .exc_value , args .exc_traceback ),
54+ )
55+
56+ threading .excepthook = thread_exc_hook
57+
58+ def unraisable_hook (args : sys .UnraisableHookArgs ) -> None :
59+ logging .getLogger ("uncaught" ).error (
60+ "Unraisable exception in %r" ,
61+ args .object ,
62+ exc_info = _safe_exc_info (args .exc_type , args .exc_value , args .exc_traceback ),
63+ )
64+
65+ sys .unraisablehook = unraisable_hook
66+
67+
68+ def setup_bootstrap_logging () -> None :
69+ root = logging .getLogger ()
70+ if root .handlers :
71+ _install_exception_hooks ()
72+ return
73+
74+ fmt = logging .Formatter ("%(asctime)s ¦ %(levelname)s ¦ %(name)s ¦ %(message)s" )
75+ sh = logging .StreamHandler (sys .stderr )
76+ sh .setLevel (logging .INFO )
77+ sh .setFormatter (fmt )
78+ root .setLevel (logging .DEBUG )
79+ root .addHandler (sh )
80+ _install_exception_hooks ()
2081
2182
2283def setup_logging (_app : QApplication ) -> Path :
23- global _configured , _log_file_path
84+ global _configured , _log_file_path , _crash_log_file
2485 if _configured :
2586 assert _log_file_path is not None
2687 return _log_file_path
@@ -53,21 +114,7 @@ def setup_logging(_app: QApplication) -> Path:
53114 sh .setFormatter (fmt )
54115 root .addHandler (sh )
55116
56- def exc_hook (exc_type , exc , tb ):
57- logging .getLogger ("uncaught" ).critical (
58- "Uncaught exception" ,
59- exc_info = (exc_type , exc , tb ),
60- )
61-
62- sys .excepthook = exc_hook
63-
64- def thread_exc_hook (args : threading .ExceptHookArgs ) -> None :
65- logging .getLogger ("uncaught" ).critical (
66- "Uncaught thread exception" ,
67- exc_info = (args .exc_type , args .exc_value , args .exc_traceback ),
68- )
69-
70- threading .excepthook = thread_exc_hook
117+ _install_exception_hooks ()
71118
72119 def qt_handler (mode : QtMsgType , context : QMessageLogContext , message : str ) -> None :
73120 if mode == QtMsgType .QtDebugMsg :
@@ -86,6 +133,19 @@ def qt_handler(mode: QtMsgType, context: QMessageLogContext, message: str) -> No
86133 logging .getLogger ("qt" ).log (level , "%s%s" , message , suffix )
87134
88135 qInstallMessageHandler (qt_handler )
136+ _app .aboutToQuit .connect (
137+ lambda : logging .getLogger ("clinical_dbs_annotator" ).info ("Application shutdown" )
138+ )
139+
140+ crash_log_path = log_dir / "clinical-dbs-annotator-crash.log"
141+ try :
142+ _crash_log_file = open (crash_log_path , "a" , encoding = "utf-8" )
143+ faulthandler .enable (file = _crash_log_file , all_threads = True )
144+ except Exception :
145+ logging .getLogger ("clinical_dbs_annotator" ).exception (
146+ "Failed to enable faulthandler with crash log %s" ,
147+ crash_log_path ,
148+ )
89149
90150 logging .getLogger ("clinical_dbs_annotator" ).info (
91151 "Started v%s Python %s | %s | log=%s" ,
0 commit comments