Add crash guard, logging, and defensive error handling to prevent lockout
Global sys.excepthook unlocks the session on unhandled exceptions. Structured logging to stderr and optional file at /var/cache/moonlock/. Window creation, CSS loading, and fingerprint start wrapped in try/except with automatic session unlock when all windows fail.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
# ABOUTME: fprintd D-Bus integration for fingerprint authentication.
|
||||
# ABOUTME: Provides FingerprintListener that runs async in the GLib mainloop.
|
||||
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import gi
|
||||
gi.require_version("Gio", "2.0")
|
||||
from gi.repository import Gio, GLib
|
||||
@@ -84,16 +87,31 @@ class FingerprintListener:
|
||||
|
||||
self._on_success = on_success
|
||||
self._on_failure = on_failure
|
||||
self._running = True
|
||||
|
||||
self._device_proxy.Claim("(s)", username)
|
||||
try:
|
||||
self._device_proxy.Claim("(s)", username)
|
||||
except GLib.Error as e:
|
||||
logger.error("Failed to claim fingerprint device: %s", e.message)
|
||||
return
|
||||
|
||||
# Connect to the VerifyStatus signal
|
||||
self._signal_id = self._device_proxy.connect(
|
||||
"g-signal", self._on_signal
|
||||
)
|
||||
|
||||
self._device_proxy.VerifyStart("(s)", "any")
|
||||
try:
|
||||
self._device_proxy.VerifyStart("(s)", "any")
|
||||
except GLib.Error as e:
|
||||
logger.error("Failed to start fingerprint verification: %s", e.message)
|
||||
self._device_proxy.disconnect(self._signal_id)
|
||||
self._signal_id = None
|
||||
try:
|
||||
self._device_proxy.Release()
|
||||
except GLib.Error:
|
||||
pass
|
||||
return
|
||||
|
||||
self._running = True
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop listening and release the device."""
|
||||
|
||||
+83
-18
@@ -1,14 +1,21 @@
|
||||
# ABOUTME: Entry point for Moonlock — sets up GTK Application and ext-session-lock-v1.
|
||||
# ABOUTME: Handles CLI invocation, session locking, and multi-monitor support.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
|
||||
# gtk4-layer-shell must be loaded before libwayland-client
|
||||
_LAYER_SHELL_LIB = "/usr/lib/libgtk4-layer-shell.so"
|
||||
_existing_preload = os.environ.get("LD_PRELOAD", "")
|
||||
if _LAYER_SHELL_LIB not in _existing_preload and os.path.exists(_LAYER_SHELL_LIB):
|
||||
_is_testing = "pytest" in sys.modules or "unittest" in sys.modules
|
||||
if (
|
||||
not _is_testing
|
||||
and _LAYER_SHELL_LIB not in _existing_preload
|
||||
and os.path.exists(_LAYER_SHELL_LIB)
|
||||
):
|
||||
os.environ["LD_PRELOAD"] = f"{_existing_preload}:{_LAYER_SHELL_LIB}".lstrip(":")
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "moonlock.main"] + sys.argv[1:])
|
||||
|
||||
@@ -21,6 +28,8 @@ from moonlock.config import load_config
|
||||
from moonlock.fingerprint import FingerprintListener
|
||||
from moonlock.lockscreen import LockscreenWindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ext-session-lock-v1 via gtk4-layer-shell
|
||||
try:
|
||||
gi.require_version("Gtk4SessionLock", "1.0")
|
||||
@@ -29,6 +38,33 @@ try:
|
||||
except (ValueError, ImportError):
|
||||
HAS_SESSION_LOCK = False
|
||||
|
||||
_LOG_DIR = Path("/var/cache/moonlock")
|
||||
_LOG_FILE = _LOG_DIR / "moonlock.log"
|
||||
|
||||
|
||||
def _setup_logging() -> None:
|
||||
"""Configure logging to stderr and optionally to a log file."""
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
stderr_handler = logging.StreamHandler(sys.stderr)
|
||||
stderr_handler.setLevel(logging.INFO)
|
||||
stderr_handler.setFormatter(formatter)
|
||||
root.addHandler(stderr_handler)
|
||||
|
||||
if _LOG_DIR.is_dir():
|
||||
try:
|
||||
file_handler = logging.FileHandler(_LOG_FILE)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(formatter)
|
||||
root.addHandler(file_handler)
|
||||
except PermissionError:
|
||||
logger.warning("Cannot write to %s", _LOG_FILE)
|
||||
|
||||
|
||||
class MoonlockApp(Gtk.Application):
|
||||
"""GTK Application for the Moonlock lockscreen."""
|
||||
@@ -62,15 +98,22 @@ class MoonlockApp(Gtk.Application):
|
||||
|
||||
for i in range(monitors.get_n_items()):
|
||||
monitor = monitors.get_item(i)
|
||||
window = LockscreenWindow(
|
||||
application=self,
|
||||
unlock_callback=self._unlock,
|
||||
config=self._config,
|
||||
fingerprint_listener=fp_listener,
|
||||
)
|
||||
self._lock_instance.assign_window_to_monitor(window, monitor)
|
||||
window.present()
|
||||
self._windows.append(window)
|
||||
try:
|
||||
window = LockscreenWindow(
|
||||
application=self,
|
||||
unlock_callback=self._unlock,
|
||||
config=self._config,
|
||||
fingerprint_listener=fp_listener,
|
||||
)
|
||||
self._lock_instance.assign_window_to_monitor(window, monitor)
|
||||
window.present()
|
||||
self._windows.append(window)
|
||||
except Exception:
|
||||
logger.exception("Failed to create lockscreen window for monitor %d", i)
|
||||
|
||||
if not self._windows:
|
||||
logger.critical("No lockscreen windows created — unlocking session to prevent lockout")
|
||||
self._lock_instance.unlock()
|
||||
|
||||
def _activate_without_lock(self) -> None:
|
||||
"""Fallback for development — no session lock, just a window."""
|
||||
@@ -90,19 +133,41 @@ class MoonlockApp(Gtk.Application):
|
||||
|
||||
def _load_css(self) -> None:
|
||||
"""Load the CSS stylesheet for the lockscreen."""
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_path = files("moonlock") / "style.css"
|
||||
css_provider.load_from_path(str(css_path))
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
try:
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_path = files("moonlock") / "style.css"
|
||||
css_provider.load_from_path(str(css_path))
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to load CSS stylesheet")
|
||||
|
||||
|
||||
def _install_crash_guard(app: MoonlockApp) -> None:
|
||||
"""Install a global exception handler that unlocks the session on crash."""
|
||||
_original_excepthook = sys.excepthook
|
||||
|
||||
def _crash_guard(exc_type, exc_value, exc_tb):
|
||||
logger.critical("Unhandled exception — unlocking session to prevent lockout", exc_info=(exc_type, exc_value, exc_tb))
|
||||
if app._lock_instance:
|
||||
try:
|
||||
app._lock_instance.unlock()
|
||||
except Exception:
|
||||
pass
|
||||
_original_excepthook(exc_type, exc_value, exc_tb)
|
||||
|
||||
sys.excepthook = _crash_guard
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the Moonlock application."""
|
||||
_setup_logging()
|
||||
logger.info("Moonlock starting")
|
||||
app = MoonlockApp()
|
||||
_install_crash_guard(app)
|
||||
app.run(sys.argv)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user