git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

src/server/sleep_lock.rs (5.5K)

  1 //! Lock the session before the machine sleeps, whoever puts it to sleep.
  2 //!
  3 //! The idle timeout locks before its own sleep (`IdleManager::lock_then_sleep`),
  4 //! but most sleeps are not ours: closing the lid, the power key, `systemctl
  5 //! suspend` from a terminal are all logind's. Until 2026-10-01 none of them
  6 //! locked anything, so a laptop opened after any of them showed the desktop.
  7 //!
  8 //! logind's answer is a DELAY inhibitor: hold one, and before sleeping logind
  9 //! emits `PrepareForSleep(true)` and waits — up to `InhibitDelayMaxSec`, 5s by
 10 //! default — for the holder to close it. This thread holds one, and on the
 11 //! signal asks the compositor to lock over its own control socket (`lock`,
 12 //! which replies once every output shows the locked scene), then lets go. On
 13 //! `PrepareForSleep(false)` — the resume — it takes a fresh inhibitor for the
 14 //! next sleep.
 15 //!
 16 //! The control socket is the way in because it already is the bridge from a
 17 //! thread to the main loop, and a `lock` reply that waits for the lock is
 18 //! exactly the "done" this needs. Failure is not silent and not fatal: no
 19 //! system bus, or logind refusing, is logged and retried, and the idle path
 20 //! keeps locking on its own.
 21 
 22 use std::io::{Read, Write};
 23 use std::time::Duration;
 24 
 25 /// Started only for a real seat (a DRM session): a headless shadow has no
 26 /// lid, and taking a delay inhibitor on the real system bus from every
 27 /// shadow would make each suspend wait on them.
 28 pub fn spawn(display_socket: Option<String>) {
 29     let socket = crate::ipc_server::get_ipc_socket_path(display_socket.as_deref());
 30     let spawned = std::thread::Builder::new()
 31         .name("cce-sleep-lock".to_string())
 32         .spawn(move || loop {
 33             if let Err(e) = run(&socket) {
 34                 log::warn!("lock-before-sleep: {e}; retrying in 30s");
 35             }
 36             std::thread::sleep(Duration::from_secs(30));
 37         });
 38     if let Err(e) = spawned {
 39         log::error!("lock-before-sleep: could not start its thread: {e}");
 40     }
 41 }
 42 
 43 fn run(socket: &str) -> Result<(), String> {
 44     let conn = zbus::blocking::Connection::system().map_err(|e| format!("no system bus: {e}"))?;
 45     let logind = zbus::blocking::Proxy::new(
 46         &conn,
 47         "org.freedesktop.login1",
 48         "/org/freedesktop/login1",
 49         "org.freedesktop.login1.Manager",
 50     )
 51     .map_err(|e| format!("no logind: {e}"))?;
 52     // Subscribed before the first inhibitor is taken, so no PrepareForSleep
 53     // can fall between holding one and listening for it.
 54     let mut signals = logind
 55         .receive_signal("PrepareForSleep")
 56         .map_err(|e| format!("cannot watch PrepareForSleep: {e}"))?;
 57 
 58     loop {
 59         let inhibitor: zbus::zvariant::OwnedFd = logind
 60             .call("Inhibit", &("sleep", "cce", "Lock the screen before sleep", "delay"))
 61             .map_err(|e| format!("logind refused a sleep inhibitor: {e}"))?;
 62         log::info!("lock-before-sleep: holding a sleep delay");
 63 
 64         wait_for(&mut signals, true)?;
 65         log::info!("lock-before-sleep: the system is going to sleep; locking");
 66         match request(socket, "lock") {
 67             Ok(reply) => log::info!("lock-before-sleep: {}", reply.trim()),
 68             // The sleep goes ahead either way: logind stops waiting at its
 69             // own deadline, and holding on would only spend that.
 70             Err(e) => log::error!("lock-before-sleep: the lock request failed: {e}"),
 71         }
 72         drop(inhibitor);
 73 
 74         wait_for(&mut signals, false)?;
 75         log::info!("lock-before-sleep: resumed");
 76     }
 77 }
 78 
 79 /// Block until `PrepareForSleep(going)`.
 80 fn wait_for(signals: &mut zbus::blocking::proxy::SignalIterator<'_>, going: bool) -> Result<(), String> {
 81     for msg in signals.by_ref() {
 82         match msg.body().deserialize::<bool>() {
 83             Ok(v) if v == going => return Ok(()),
 84             Ok(_) => {}
 85             Err(e) => log::warn!("lock-before-sleep: unreadable PrepareForSleep: {e}"),
 86         }
 87     }
 88     Err("the system bus closed the PrepareForSleep stream".to_string())
 89 }
 90 
 91 /// One control-socket command, answered: the same exchange `ccectl` makes.
 92 fn request(socket: &str, command: &str) -> std::io::Result<String> {
 93     let mut stream = std::os::unix::net::UnixStream::connect(socket)?;
 94     stream.set_read_timeout(Some(Duration::from_secs(5)))?;
 95     stream.write_all(command.as_bytes())?;
 96     stream.write_all(b"\n")?;
 97     let mut reply = String::new();
 98     stream.read_to_string(&mut reply)?;
 99     Ok(reply)
100 }
101 
102 #[cfg(test)]
103 mod tests {
104     /// Talks to the real logind, so it is opt-in: `cargo test -p cce-fx --lib
105     /// sleep_lock -- --ignored`. It takes a delay inhibitor and drops it at
106     /// once (nothing sleeps), and checks the signal subscription the thread
107     /// waits on can be made — the two calls a shadow, with no seat, never
108     /// exercises.
109     #[test]
110     #[ignore]
111     fn logind_grants_a_sleep_delay_and_the_signal_can_be_watched() {
112         let conn = zbus::blocking::Connection::system().expect("system bus");
113         let logind = zbus::blocking::Proxy::new(
114             &conn,
115             "org.freedesktop.login1",
116             "/org/freedesktop/login1",
117             "org.freedesktop.login1.Manager",
118         )
119         .unwrap();
120         let _signals = logind.receive_signal("PrepareForSleep").expect("PrepareForSleep subscription");
121         let fd: zbus::zvariant::OwnedFd = logind
122             .call("Inhibit", &("sleep", "cce-test", "test: dropped at once", "delay"))
123             .expect("a sleep delay inhibitor");
124         use std::os::fd::AsRawFd;
125         assert!(std::os::fd::AsFd::as_fd(&fd).as_raw_fd() >= 0);
126     }
127 }