fix: handle monitor hotplug to survive suspend/resume (v0.6.9)

moonlock crashed with segfault in libgtk-4.so after suspend/resume when
HDMI monitors disconnected and reconnected, invalidating GDK monitor
objects that statically created windows still referenced.

Replace manual monitor iteration with connect_monitor signal (v1_2) that
fires both at lock time and on hotplug. Windows are now created on demand
per monitor event and auto-unmap when their monitor disappears.
This commit is contained in:
2026-04-09 14:48:06 +02:00
parent b89435b810
commit b621b4e9fe
5 changed files with 93 additions and 42 deletions
+81 -36
View File
@@ -59,17 +59,18 @@ fn activate(app: &gtk::Application) {
fn activate_with_session_lock(
app: &gtk::Application,
display: &gdk::Display,
_display: &gdk::Display,
config: &config::Config,
) {
let lock = gtk4_session_lock::Instance::new();
lock.lock();
// Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition
let bg_texture = config::resolve_background_path(config)
.and_then(|path| lockscreen::load_background_texture(&path));
let monitors = display.monitors();
// 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.
@@ -91,50 +92,89 @@ fn activate_with_session_lock(
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));
// Create all monitor windows immediately — no D-Bus calls here
let mut all_handles = Vec::new();
let mut created_any = false;
for i in 0..monitors.n_items() {
if let Some(monitor) = monitors
.item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{
// 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 handles = lockscreen::create_lockscreen_window(
bg_texture.as_ref(),
config,
app,
bg_texture.as_ref().as_ref(),
&config,
&app,
unlock_callback.clone(),
&blur_cache,
&avatar_cache,
);
lock.assign_window_to_monitor(&handles.window, &monitor);
lock_for_signal.assign_window_to_monitor(&handles.window, monitor);
handles.window.present();
all_handles.push(handles);
created_any = true;
}
}
if !created_any {
log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
std::process::exit(1);
}
// 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);
}
));
lock.lock();
// Async fprintd initialization — runs after windows are visible
if config.fingerprint_enabled {
init_fingerprint_async(all_handles);
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 UI wired up.
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
/// 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;
let handles = all_handles.borrow();
if handles.is_empty() {
return;
}
// Use the first monitor's username to check enrollment
let username = &all_handles[0].username;
let username = &handles[0].username;
if username.is_empty() {
return;
}
@@ -146,13 +186,16 @@ fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
let fp_rc = Rc::new(RefCell::new(listener));
// Show fingerprint label on all monitors
for handles in &all_handles {
lockscreen::show_fingerprint_label(handles, &fp_rc);
// Show fingerprint label on all existing monitors
for h in handles.iter() {
lockscreen::show_fingerprint_label(h, &fp_rc);
}
// Start verification listener on the first monitor only
lockscreen::start_fingerprint(&all_handles[0], &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);
});
}
@@ -184,7 +227,9 @@ fn activate_without_lock(
// Async fprintd initialization for development mode
if config.fingerprint_enabled {
init_fingerprint_async(vec![handles]);
let all_handles = Rc::new(RefCell::new(vec![handles]));
let shared_fp = Rc::new(RefCell::new(None));
init_fingerprint_async(all_handles, shared_fp);
}
}