Replace crash guard with SIGUSR1 unlock and crash logging

Remove sys.excepthook that unlocked on crash — this violated
ext-session-lock-v1 security model where the compositor must keep
the screen locked if the client dies (per protocol spec and hyprlock
reference). Now: crashes are logged but session stays locked.
SIGUSR1 handler added for external recovery (e.g. wrapper script).
This commit is contained in:
2026-03-26 21:11:42 +01:00
parent 3f31387305
commit 5fda0dce0c
3 changed files with 48 additions and 48 deletions
+27 -32
View File
@@ -170,10 +170,10 @@ class TestDefensiveWindowCreation:
@patch("moonlock.main.FingerprintListener")
@patch("moonlock.main.Gdk.Display.get_default")
@patch("moonlock.main.Gtk4SessionLock")
def test_all_windows_fail_unlocks_session(
def test_all_windows_fail_does_not_unlock_session(
self, mock_session_lock, mock_display, mock_fp, mock_window_cls
):
"""If ALL windows fail to create, session should be unlocked to prevent lockout."""
"""If ALL windows fail, session stays locked (compositor policy)."""
MoonlockApp, _ = _import_main()
app = MoonlockApp.__new__(MoonlockApp)
app._config = MagicMock()
@@ -195,48 +195,43 @@ class TestDefensiveWindowCreation:
with patch("moonlock.main.logger"):
app._activate_with_session_lock()
# Session should have been unlocked to prevent permanent lockout
lock_instance.unlock.assert_called_once()
# Session must NOT be unlocked — compositor keeps screen locked
lock_instance.unlock.assert_not_called()
class TestCrashGuard:
"""Tests for the global exception handler that prevents lockout on crash."""
class TestSignalHandlers:
"""Tests for signal handlers and excepthook behavior."""
def test_crash_guard_unlocks_session_on_unhandled_exception(self):
"""sys.excepthook should unlock the session when an exception reaches it."""
def test_sigusr1_triggers_unlock(self):
"""SIGUSR1 should schedule an unlock via GLib.idle_add."""
MoonlockApp, _ = _import_main()
from moonlock.main import _install_crash_guard
from moonlock.main import _install_signal_handlers
import signal
app = MoonlockApp.__new__(MoonlockApp)
app._lock_instance = MagicMock()
_install_signal_handlers(app)
handler = signal.getsignal(signal.SIGUSR1)
assert handler is not signal.SIG_DFL
def test_excepthook_does_not_unlock(self):
"""Unhandled exceptions must NOT unlock the session."""
MoonlockApp, _ = _import_main()
from moonlock.main import _install_signal_handlers
app = MoonlockApp.__new__(MoonlockApp)
app._lock_instance = MagicMock()
original_hook = sys.excepthook
_install_crash_guard(app)
try:
# Simulate an unhandled exception reaching the hook
with patch("moonlock.main.logger"):
sys.excepthook(RuntimeError, RuntimeError("segfault"), None)
app._lock_instance.unlock.assert_called_once()
finally:
sys.excepthook = original_hook
def test_crash_guard_survives_unlock_failure(self):
"""Crash guard should not raise even if unlock() itself fails."""
MoonlockApp, _ = _import_main()
from moonlock.main import _install_crash_guard
app = MoonlockApp.__new__(MoonlockApp)
app._lock_instance = MagicMock()
app._lock_instance.unlock.side_effect = Exception("protocol error")
original_hook = sys.excepthook
_install_crash_guard(app)
_install_signal_handlers(app)
try:
with patch("moonlock.main.logger"):
# Should not raise despite unlock failure
sys.excepthook(RuntimeError, RuntimeError("crash"), None)
# Must NOT have called unlock
app._lock_instance.unlock.assert_not_called()
finally:
sys.excepthook = original_hook