Skip to content

Crash Detection

Sentinel-based crash detection system that identifies unclean shutdowns and offers crash reporting on the next startup.

How It Works

  1. Startup: initialize_crash_detection() writes a JSON sentinel file
  2. Clean exit: Sentinel is removed via atexit handler
  3. Crash: Sentinel persists because atexit is skipped (or _crash_detected flag preserves it)
  4. Next startup: check_and_handle_previous_crash() finds the sentinel and shows a report dialog

API

from src.shared_services.crash_detection.api import (
    initialize_crash_detection,
    check_and_handle_previous_crash,
    write_crash_details_for_current_exception,
)
  • initialize_crash_detection() -- call early in startup, after initialize_paths()
  • check_and_handle_previous_crash(parent) -- call after QApplication + theme init (use QTimer.singleShot)
  • write_crash_details_for_current_exception() -- call from top-level except blocks

Components

File Responsibility
api.py Public API (three functions above)
crash_sentinel.py Sentinel file write/read/remove
crash_hook.py sys.excepthook + atexit registration
crash_report_builder.py Formats crash data into a readable report
crash_report_dialog.py CrashReportDialog -- send to GitHub, save to Desktop, or ignore

Files

  • Sentinel: .app_data/crash_detection/crash_sentinel.json
  • Details: .app_data/crash_detection/crash_details.json

API Reference

src.shared_services.crash_detection.api

Public API for the crash detection system.

USAGE

Early in startup (after initialize_paths, before risky code):

from src.shared_services.crash_detection.api import initialize_crash_detection initialize_crash_detection()

After QApplication + theme init (before or after OAuth):

from src.shared_services.crash_detection.api import check_and_handle_previous_crash check_and_handle_previous_crash()

In except blocks that catch before sys.excepthook fires:

from src.shared_services.crash_detection.api import write_crash_details_for_current_exception write_crash_details_for_current_exception()

check_and_handle_previous_crash(parent=None)

Show crash report dialog if the previous session crashed.

Call after QApplication and theme are initialized so the dialog renders correctly. Safe to call before or after OAuth.

Parameters:

Name Type Description Default
parent

Optional parent widget for the dialog.

None
Source code in src\shared_services\crash_detection\api.py
def check_and_handle_previous_crash(parent=None) -> None:
    """Show crash report dialog if the previous session crashed.

    Call after QApplication and theme are initialized so the dialog
    renders correctly. Safe to call before or after OAuth.

    Args:
        parent: Optional parent widget for the dialog.
    """
    global _previous_crash_data

    if _previous_crash_data is None:
        return

    crash_data = _previous_crash_data
    _previous_crash_data = None  # Only show once

    from src.shared_services.crash_detection.crash_report_dialog import CrashReportDialog

    dialog = CrashReportDialog(crash_data, parent=parent)
    dialog.show()

initialize_crash_detection()

Write sentinel and install exception hooks.

Checks for a previous crash BEFORE writing the new sentinel, then stores crash data for later retrieval by check_and_handle_previous_crash().

Call once, early in startup, after initialize_paths().

Source code in src\shared_services\crash_detection\api.py
def initialize_crash_detection() -> None:
    """Write sentinel and install exception hooks.

    Checks for a previous crash BEFORE writing the new sentinel,
    then stores crash data for later retrieval by
    check_and_handle_previous_crash().

    Call once, early in startup, after initialize_paths().
    """
    global _sentinel, _hook, _previous_crash_data

    _sentinel = CrashSentinel()

    # Check for previous crash BEFORE overwriting with the new sentinel
    _previous_crash_data = _sentinel.check_previous_crash()

    if _previous_crash_data is not None:
        from src.shared_services.logging.logger_factory import get_logger
        logger = get_logger()
        exc = _previous_crash_data.get("exception_type", "unknown")
        logger.info(f"Previous crash detected: {exc}")

    # Write the new sentinel for this session
    _sentinel.write_sentinel()

    # Install hooks (excepthook + atexit)
    _hook = CrashHook(_sentinel)
    _hook.install()

write_crash_details_for_current_exception()

Write crash details for the currently active exception.

Call from except blocks that catch exceptions before sys.excepthook fires (e.g. the try/except in GehaSoftwareHub.py). Also marks the crash so atexit preserves the sentinel.

Source code in src\shared_services\crash_detection\api.py
def write_crash_details_for_current_exception() -> None:
    """Write crash details for the currently active exception.

    Call from except blocks that catch exceptions before
    sys.excepthook fires (e.g. the try/except in GehaSoftwareHub.py).
    Also marks the crash so atexit preserves the sentinel.
    """
    if _sentinel is None:
        return

    if _hook is not None:
        _hook.mark_crash()

    exc_type, exc_value, exc_tb = sys.exc_info()
    if exc_type is not None:
        _sentinel.write_crash_details(exc_type, exc_value, exc_tb)