215ab0a984
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.
325 lines
11 KiB
Rust
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: >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<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: >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<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();
|
|
}
|