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 }