Files
moonlock/src/main.rs
T
nevaforget 215ab0a984 chore: drop unlock diagnostic instrumentation (v0.6.16)
The per-unlock window-state dump was scaffolding to prove the stale-window
hypothesis behind v0.6.15. With the fix in place, normal logging covers
validation: the prune handler already logs on monitor removal, and
coredumpctl shows whether the crash recurs. Remove the diagnostic block
and its all_handles_dbg clone; restore all_handles to its original spot.
2026-06-02 17:05:34 +02:00

325 lines
11 KiB
Rust

// 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: &gtk::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: &gtk::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<Option<gdk::Texture>> = 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<dyn Fn()> = 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<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = 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<RefCell<Vec<lockscreen::LockscreenHandles>>> =
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<RefCell<Option<Rc<RefCell<FingerprintListener>>>>> =
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<RefCell<Vec<lockscreen::LockscreenHandles>>>,
shared_fp: Rc<RefCell<Option<Rc<RefCell<FingerprintListener>>>>>,
) {
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: &gtk::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<dyn Fn()> = 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();
}