Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion graphlink_app/graphlink_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import graphlink_licensing
from graphlink_config import apply_theme, set_current_model, sync_ollama_task_models
from graphlink_logging import configure_logging
from graphlink_crash import install_crash_handlers, mark_running, previous_run_crashed
from graphlink_crash import (
install_crash_handlers,
mark_running,
previous_run_crashed,
uninstall_crash_handlers,
)
from graphlink_version import APP_VERSION

def main():
Expand All @@ -19,6 +24,8 @@ def main():

app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(True)
# Remove Python/Qt crash callbacks before Qt destroys its Python wrappers.
app.aboutToQuit.connect(uninstall_crash_handlers)

# Use the new SettingsManager (formerly LicenseManager)
settings_manager = graphlink_licensing.SettingsManager()
Expand Down
37 changes: 37 additions & 0 deletions graphlink_app/graphlink_crash.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,43 @@ def install_crash_handlers(version="unknown", crash_dir=None):
pass


def uninstall_crash_handlers():
"""Restore process-global handlers before Qt/PySide is torn down.

Crash reporting is installed before ``QApplication`` exists, so the normal
application shutdown path must explicitly remove the Qt message callback
while Qt's Python wrappers are still alive. This is also required by the
test suite: leaving a callback that closes over PySide objects installed
until interpreter shutdown can turn an otherwise passing Qt test run into
a non-zero process exit.
"""
global _installed, _faulthandler_file

sys.excepthook = sys.__excepthook__
threading.excepthook = threading.__excepthook__

try:
from PySide6.QtCore import qInstallMessageHandler

qInstallMessageHandler(None)
except (ImportError, AttributeError, RuntimeError, SystemError, TypeError):
pass

try:
faulthandler.disable()
except (AttributeError, RuntimeError, SystemError, ValueError):
pass

if _faulthandler_file is not None:
try:
_faulthandler_file.close()
except (AttributeError, OSError, ValueError):
pass
_faulthandler_file = None

_installed = False


# --- "did the previous run crash" sentinel ---
#
# A JSON file at ~/.graphlink/running.lock is written at startup (mark_running) and
Expand Down
20 changes: 11 additions & 9 deletions graphlink_app/tests/test_crash_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,7 @@ def test_url_targets_the_repo_and_embeds_the_report(self):

class TestInstallCrashHandlers:
def teardown_method(self):
sys.excepthook = sys.__excepthook__
threading.excepthook = threading.__excepthook__
graphlink_crash._installed = False
if graphlink_crash._faulthandler_file is not None:
try:
graphlink_crash._faulthandler_file.close()
except Exception:
pass
graphlink_crash._faulthandler_file = None
graphlink_crash.uninstall_crash_handlers()

def test_installs_a_non_default_excepthook(self, tmp_path):
graphlink_crash.install_crash_handlers(version="v1", crash_dir=tmp_path)
Expand All @@ -187,6 +179,16 @@ def test_is_idempotent(self, tmp_path):

assert sys.excepthook is first_hook

def test_uninstall_restores_process_global_handlers(self, tmp_path):
graphlink_crash.install_crash_handlers(version="v1", crash_dir=tmp_path)

graphlink_crash.uninstall_crash_handlers()

assert sys.excepthook is sys.__excepthook__
assert threading.excepthook is threading.__excepthook__
assert graphlink_crash._installed is False
assert graphlink_crash._faulthandler_file is None

def test_the_installed_excepthook_writes_a_report(self, tmp_path, monkeypatch):
monkeypatch.setattr(graphlink_crash, "_crash_dir", lambda base_dir=None: tmp_path)
graphlink_crash.install_crash_handlers(version="v1", crash_dir=tmp_path)
Expand Down
Loading