// ABOUTME: Entry point for Moonlock — secure Wayland lockscreen. // ABOUTME: Sets up GTK Application, ext-session-lock-v1, Panic-Hook, and multi-monitor windows. mod auth; mod config; mod fingerprint; mod i18n; mod lockscreen; mod power; mod users; use gdk4 as gdk; use gtk4::prelude::*; use gtk4::{self as gtk, gio}; use gtk4_session_lock; use std::cell::{Cell, RefCell}; use std::rc::Rc; use crate::fingerprint::FingerprintListener; fn load_css(display: &gdk::Display) { let css_provider = gtk::CssProvider::new(); css_provider.load_from_resource("/dev/moonarch/moonlock/style.css"); gtk::style_context_add_provider_for_display( display, &css_provider, gtk::STYLE_PROVIDER_PRIORITY_USER, ); } fn activate(app: >k::Application) { let display = match gdk::Display::default() { Some(d) => d, None => { log::error!("No display available — cannot start lockscreen UI"); return; } }; load_css(&display); let config = config::load_config(None); if gtk4_session_lock::is_supported() { activate_with_session_lock(app, &display, &config); } else { #[cfg(debug_assertions)] { log::warn!("ext-session-lock-v1 not supported — running in development mode"); activate_without_lock(app, &config); } #[cfg(not(debug_assertions))] { log::error!("ext-session-lock-v1 not supported — refusing to run without session lock"); std::process::exit(1); } } } fn activate_with_session_lock( app: >k::Application, display: &gdk::Display, config: &config::Config, ) { let lock = gtk4_session_lock::Instance::new(); // Load wallpaper before lock — connect_monitor fires during lock() and needs the // texture. This means disk I/O happens before locking, but loading a local JPEG // is fast enough that the delay is negligible. let bg_texture: Rc> = Rc::new( config::resolve_background_path(config) .and_then(|path| lockscreen::load_background_texture(&path)), ); // Shared unlock callback — unlocks session and quits. // Guard prevents double-unlock if PAM and fingerprint succeed simultaneously. let lock_clone = lock.clone(); let app_clone = app.clone(); let already_unlocked = Rc::new(Cell::new(false)); let au = already_unlocked.clone(); let unlock_callback: Rc = Rc::new(move || { if au.get() { log::debug!("Unlock already triggered, ignoring duplicate"); return; } au.set(true); lock_clone.unlock(); app_clone.quit(); }); // Shared caches for multi-monitor — first monitor renders, rest reuse let blur_cache: Rc>> = Rc::new(RefCell::new(None)); let avatar_cache: Rc>> = Rc::new(RefCell::new(None)); // Shared config for use in the monitor signal handler let config = Rc::new(config.clone()); // Shared handles list — populated by connect_monitor, read by fingerprint init let all_handles: Rc>> = Rc::new(RefCell::new(Vec::new())); // Shared fingerprint listener — None until async init completes. // The monitor handler checks this to wire up FP labels on hotplugged monitors. let shared_fp: Rc>>>> = Rc::new(RefCell::new(None)); // The ::monitor signal fires once per existing monitor at lock(), and again // whenever a monitor is hotplugged (e.g. after suspend/resume). This replaces // the old manual monitor iteration and handles hotplug automatically. let lock_for_signal = lock.clone(); lock.connect_monitor(glib::clone!( #[strong] app, #[strong] config, #[strong] bg_texture, #[strong] unlock_callback, #[strong] blur_cache, #[strong] avatar_cache, #[strong] all_handles, #[strong] shared_fp, move |_instance, monitor| { log::debug!("Monitor signal: creating lockscreen window"); let mut handles = lockscreen::create_lockscreen_window( bg_texture.as_ref().as_ref(), &config, &app, unlock_callback.clone(), &blur_cache, &avatar_cache, ); lock_for_signal.assign_window_to_monitor(&handles.window, monitor); handles.window.present(); handles.monitor = Some(monitor.clone()); // If fingerprint is already initialized, wire up the label if let Some(ref fp_rc) = *shared_fp.borrow() { lockscreen::show_fingerprint_label(&handles, fp_rc); } all_handles.borrow_mut().push(handles); } )); // connect_monitor only ADDS — it never tells us when a monitor powers off on // suspend. gtk4-session-lock then unmaps and drops its own ref to that monitor's // window, but the GtkApplication and all_handles still hold refs, so the orphaned // window survives until unlock — where gtk_window_destroy dereferences its now-NULL // monitor association and segfaults. Watch the display's monitor list and prune any // window whose monitor is no longer valid, releasing our refs (the lib doc points to // "GTK APIs" for exactly this). We release refs, not destroy — the lib already // unmapped+dereffed the window. display.monitors().connect_items_changed(glib::clone!( #[weak] app, #[strong] all_handles, move |_, _, _, _| { all_handles.borrow_mut().retain(|h| { let alive = h.monitor.as_ref().is_none_or(gdk::Monitor::is_valid); if !alive { log::info!("Monitor removed — pruning its lockscreen window"); app.remove_window(&h.window); } alive }); } )); lock.lock(); // Async fprintd initialization — runs after windows are visible if config.fingerprint_enabled { init_fingerprint_async(all_handles, shared_fp); } } /// Initialize fprintd asynchronously after windows are visible. /// Uses a single FingerprintListener shared across all monitors — /// only the first monitor's handles get the fingerprint verification wired up. /// The `shared_fp` is set after init so that the connect_monitor handler can /// wire up FP labels on monitors that appear after initialization. fn init_fingerprint_async( all_handles: Rc>>, shared_fp: Rc>>>>, ) { glib::spawn_future_local(async move { let mut listener = FingerprintListener::new(); listener.init_async().await; // Extract username without holding a borrow across the await below — // otherwise a concurrent connect_monitor signal (hotplug / suspend-resume) // that tries to borrow_mut() panics at runtime. let username = { let handles = all_handles.borrow(); if handles.is_empty() { return; } let u = handles[0].username.clone(); if u.is_empty() { return; } u }; if !listener.is_available_async(&username).await { log::debug!("fprintd not available or no enrolled fingers"); return; } let fp_rc = Rc::new(RefCell::new(listener)); // Re-borrow after the await — no further awaits in this scope, so it is // safe to hold the borrow briefly while wiring up the labels. { let handles = all_handles.borrow(); for h in handles.iter() { lockscreen::show_fingerprint_label(h, &fp_rc); } lockscreen::start_fingerprint(&handles[0], &fp_rc); } // Publish the listener so hotplugged monitors get FP labels too *shared_fp.borrow_mut() = Some(fp_rc); }); } #[cfg(debug_assertions)] fn activate_without_lock( app: >k::Application, config: &config::Config, ) { let bg_texture = config::resolve_background_path(config) .and_then(|path| lockscreen::load_background_texture(&path)); let app_clone = app.clone(); let unlock_callback: Rc = Rc::new(move || { app_clone.quit(); }); let blur_cache = Rc::new(RefCell::new(None)); let avatar_cache = Rc::new(RefCell::new(None)); let handles = lockscreen::create_lockscreen_window( bg_texture.as_ref(), config, app, unlock_callback, &blur_cache, &avatar_cache, ); handles.window.set_default_size(800, 600); handles.window.present(); // Async fprintd initialization for development mode if config.fingerprint_enabled { let all_handles = Rc::new(RefCell::new(vec![handles])); let shared_fp = Rc::new(RefCell::new(None)); init_fingerprint_async(all_handles, shared_fp); } } fn setup_logging() { match systemd_journal_logger::JournalLog::new() { Ok(logger) => { if let Err(e) = logger.install() { eprintln!("Failed to install journal logger: {e}"); } } Err(e) => { eprintln!("Failed to create journal logger: {e}"); } } // Debug level is only selectable in debug builds. Release binaries ignore // MOONLOCK_DEBUG so a session script cannot escalate log verbosity to leak // fprintd / D-Bus internals into the journal. #[cfg(debug_assertions)] let level = if std::env::var("MOONLOCK_DEBUG").is_ok() { log::LevelFilter::Debug } else { log::LevelFilter::Info }; #[cfg(not(debug_assertions))] let level = log::LevelFilter::Info; log::set_max_level(level); } fn install_panic_hook() { // Install a panic hook BEFORE starting the app. // On panic, we log but NEVER unlock. The compositor's ext-session-lock-v1 // policy keeps the screen locked when the client crashes. let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { log::error!("PANIC — screen stays locked (compositor policy): {info}"); default_hook(info); })); } fn main() { install_panic_hook(); setup_logging(); // Root check — moonlock should not run as root if nix::unistd::getuid().is_root() { log::error!("Moonlock should not run as root"); std::process::exit(1); } log::info!("Moonlock starting"); // Register compiled GResources gio::resources_register_include!("moonlock.gresource").expect("Failed to register resources"); let app = gtk::Application::builder() .application_id("dev.moonarch.moonlock") .build(); app.connect_activate(activate); app.run(); }