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

src/server/idle.rs (12.7K)

  1 // SPDX-License-Identifier: GPL-3.0-only
  2 
  3 //! Idle timeouts: turn the displays off after `display_off` seconds without
  4 //! input, and run the sleep command after `sleep` seconds. Both are 0 (off)
  5 //! until `idle { }` in config.kdl sets them.
  6 //!
  7 //! Activity is whatever `Seat::handle_activity` already counts as activity
  8 //! for the `ext-idle-notify` clients — pointer motion, buttons, axes,
  9 //! gestures, tablet, and (since this module) keys. An `idle-inhibit`
 10 //! inhibitor on a mapped surface (a video player) pauses both timers, the
 11 //! same signal `wlr_idle_notifier_v1_set_inhibited` gets.
 12 //!
 13 //! "Display off" is the soft-disable the wlr-output-power-management
 14 //! protocol already drives (`OutputStateValue::DisabledSoft` — the output
 15 //! stays in the layout, nothing is re-arranged, and no frame events fire
 16 //! while it is dark, so an idle desktop also stops rendering). Only outputs
 17 //! this module darkened (`Output::idle_off`) are woken again: one a client
 18 //! turned off with `wlopm` stays as that client left it.
 19 //!
 20 //! Resume: `systemctl suspend` returns as soon as the job is queued, so the
 21 //! command's exit says nothing. Instead the wlroots session's `active`
 22 //! signal — which fires when the seat comes back from suspend, and on a VT
 23 //! switch back — is treated as activity, so a lid-open shows the screen
 24 //! without waiting for a key.
 25 
 26 use crate::ffi;
 27 use crate::server::{Server, WlListener, wl_signal_add, wl_listener_remove};
 28 use crate::output::{Output, OutputStateValue};
 29 
 30 pub const DEFAULT_SLEEP_COMMAND: &str = "systemctl suspend";
 31 
 32 /// The `idle { }` block, in seconds; 0 disables a timeout.
 33 #[derive(Debug, Clone, PartialEq, Eq)]
 34 pub struct IdleConfig {
 35     pub display_off_s: i64,
 36     pub sleep_s: i64,
 37     pub sleep_command: Option<String>,
 38 }
 39 
 40 impl Default for IdleConfig {
 41     fn default() -> Self {
 42         Self { display_off_s: 0, sleep_s: 0, sleep_command: None }
 43     }
 44 }
 45 
 46 /// Re-arming the timers on every pointer-motion event would be a pair of
 47 /// `timerfd_settime` calls per event; once a second is plenty for timeouts
 48 /// measured in minutes.
 49 const REARM_MIN_MS: u64 = 1000;
 50 
 51 pub struct IdleManager {
 52     pub server: *mut Server,
 53     display_timer: *mut ffi::wl_event_source,
 54     sleep_timer: *mut ffi::wl_event_source,
 55     /// Timeouts in ms; 0 = disabled.
 56     display_off_ms: i64,
 57     sleep_ms: i64,
 58     /// `None` means `DEFAULT_SLEEP_COMMAND`. (An `Option<String>` is
 59     /// null-niche safe under `Server::new`'s zeroed init; a bare `String`
 60     /// is not.)
 61     sleep_command: Option<String>,
 62     /// An idle-inhibitor is active: timers are held disarmed.
 63     inhibited: bool,
 64     /// The display timeout fired and outputs were darkened by us.
 65     displays_off: bool,
 66     /// The sleep command was spawned; cleared by the next activity.
 67     sleeping: bool,
 68     /// Monotonic ms of the last (re)arm and of the last activity.
 69     armed_at_ms: u64,
 70     last_activity_ms: u64,
 71     session_active: ffi::wl_listener,
 72     session_listening: bool,
 73 }
 74 
 75 fn now_ms() -> u64 {
 76     let ts = crate::util::timestamp();
 77     ts.tv_sec as u64 * 1000 + ts.tv_nsec as u64 / 1_000_000
 78 }
 79 
 80 impl IdleManager {
 81     pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
 82         self.server = server;
 83         let event_loop = ffi::wl_display_get_event_loop((*server).wl_server);
 84         self.display_timer = ffi::wl_event_loop_add_timer(
 85             event_loop,
 86             Some(handle_display_timeout),
 87             self as *mut IdleManager as *mut _,
 88         );
 89         if self.display_timer.is_null() {
 90             return Err("Failed to create idle display timer");
 91         }
 92         self.sleep_timer = ffi::wl_event_loop_add_timer(
 93             event_loop,
 94             Some(handle_sleep_timeout),
 95             self as *mut IdleManager as *mut _,
 96         );
 97         if self.sleep_timer.is_null() {
 98             ffi::wl_event_source_remove(self.display_timer);
 99             self.display_timer = std::ptr::null_mut();
100             return Err("Failed to create idle sleep timer");
101         }
102         self.display_off_ms = 0;
103         self.sleep_ms = 0;
104         self.sleep_command = None;
105         self.inhibited = false;
106         self.displays_off = false;
107         self.sleeping = false;
108         self.armed_at_ms = 0;
109         self.last_activity_ms = now_ms();
110 
111         // Headless and nested backends have no session; only DRM does.
112         let session = (*server).session;
113         if !session.is_null() {
114             let listener = &mut self.session_active as *mut ffi::wl_listener as *mut WlListener;
115             (*listener).notify = Some(handle_session_active);
116             wl_signal_add(ffi::river_wlr_session_get_active_signal(session), &mut self.session_active);
117             self.session_listening = true;
118         }
119         Ok(())
120     }
121 
122     pub unsafe fn deinit(&mut self) {
123         if self.session_listening {
124             wl_listener_remove(&mut self.session_active);
125             self.session_listening = false;
126         }
127         if !self.display_timer.is_null() {
128             ffi::wl_event_source_remove(self.display_timer);
129             self.display_timer = std::ptr::null_mut();
130         }
131         if !self.sleep_timer.is_null() {
132             ffi::wl_event_source_remove(self.sleep_timer);
133             self.sleep_timer = std::ptr::null_mut();
134         }
135     }
136 
137     /// Apply an `idle { }` block (config load and `ccectl reload`).
138     pub unsafe fn configure(&mut self, cfg: &IdleConfig) {
139         self.display_off_ms = cfg.display_off_s.max(0) * 1000;
140         self.sleep_ms = cfg.sleep_s.max(0) * 1000;
141         self.sleep_command = cfg.sleep_command.clone();
142         log::info!(
143             "idle timeouts: display_off={}s sleep={}s command={:?}",
144             cfg.display_off_s.max(0),
145             cfg.sleep_s.max(0),
146             self.sleep_command()
147         );
148         self.rearm(true);
149     }
150 
151     pub fn sleep_command(&self) -> &str {
152         self.sleep_command.as_deref().unwrap_or(DEFAULT_SLEEP_COMMAND)
153     }
154 
155     /// Input arrived (or the session came back). Wakes darkened outputs
156     /// and restarts both countdowns.
157     pub unsafe fn on_activity(&mut self) {
158         let now = now_ms();
159         self.last_activity_ms = now;
160         let changed = self.displays_off || self.sleeping;
161         if self.displays_off {
162             self.set_displays(true);
163         }
164         self.sleeping = false;
165         self.rearm(changed);
166     }
167 
168     /// From `IdleInhibitManager::check_active`: an inhibitor appeared or
169     /// the last one went away.
170     pub unsafe fn set_inhibited(&mut self, inhibited: bool) {
171         if self.inhibited == inhibited {
172             return;
173         }
174         self.inhibited = inhibited;
175         log::debug!("idle: inhibited={}", inhibited);
176         self.rearm(true);
177     }
178 
179     /// Arm (or disarm, when inhibited or unconfigured) both timers from
180     /// now. Throttled unless `force`: pointer motion calls this per event.
181     unsafe fn rearm(&mut self, force: bool) {
182         let now = now_ms();
183         if !force && now.saturating_sub(self.armed_at_ms) < REARM_MIN_MS {
184             return;
185         }
186         self.armed_at_ms = now;
187         let active = !self.inhibited;
188         let display_ms = if active { self.display_off_ms } else { 0 };
189         let sleep_ms = if active { self.sleep_ms } else { 0 };
190         if !self.display_timer.is_null() {
191             ffi::wl_event_source_timer_update(self.display_timer, display_ms.min(i32::MAX as i64) as i32);
192         }
193         if !self.sleep_timer.is_null() {
194             ffi::wl_event_source_timer_update(self.sleep_timer, sleep_ms.min(i32::MAX as i64) as i32);
195         }
196     }
197 
198     /// Darken (`on == false`) every enabled output, or wake the ones this
199     /// module darkened. Goes through the same scheduled-state path as the
200     /// output-power protocol; the next transaction commits it.
201     pub unsafe fn set_displays(&mut self, on: bool) {
202         let server = &mut *self.server;
203         let head = &mut server.om.outputs as *mut ffi::wl_list;
204         let mut link = (*head).next;
205         let mut touched = 0;
206         while link != head {
207             let output = &mut *crate::container_of!(link, Output, link);
208             link = (*link).next;
209             if output.wlr_output.is_null() {
210                 continue;
211             }
212             if on {
213                 if output.idle_off {
214                     output.idle_off = false;
215                     if output.scheduled.state == OutputStateValue::DisabledSoft {
216                         output.scheduled.state = OutputStateValue::Enabled;
217                         touched += 1;
218                     }
219                 }
220             } else if output.scheduled.state == OutputStateValue::Enabled {
221                 output.scheduled.state = OutputStateValue::DisabledSoft;
222                 output.idle_off = true;
223                 touched += 1;
224             }
225         }
226         self.displays_off = !on;
227         log::info!("idle: displays {} ({} output(s))", if on { "on" } else { "off" }, touched);
228         if touched > 0 {
229             server.wm.dirty_windowing();
230         }
231     }
232 
233     /// Run the sleep command (`sh -c`), detached; the server's SIGCHLD
234     /// handler reaps it.
235     pub unsafe fn sleep_now(&mut self) {
236         let cmd = self.sleep_command().to_string();
237         log::info!("idle: sleeping via `{}`", cmd);
238         self.sleeping = true;
239         match nix::unistd::fork() {
240             Ok(nix::unistd::ForkResult::Child) => {
241                 crate::process::cleanup_child();
242                 let sh = std::ffi::CString::new("/bin/sh").unwrap();
243                 let dash_c = std::ffi::CString::new("-c").unwrap();
244                 let cmd_c = std::ffi::CString::new(cmd).unwrap_or_else(|_| std::ffi::CString::new("true").unwrap());
245                 let args = [sh.as_c_str(), dash_c.as_c_str(), cmd_c.as_c_str()];
246                 let _ = nix::unistd::execv(&sh, &args);
247                 std::process::exit(1);
248             }
249             Ok(nix::unistd::ForkResult::Parent { .. }) => {}
250             Err(e) => {
251                 log::error!("idle: failed to fork for sleep command: {}", e);
252                 self.sleeping = false;
253             }
254         }
255     }
256 
257     /// `ccectl idle` report.
258     pub fn status(&self) -> String {
259         let idle_s = now_ms().saturating_sub(self.last_activity_ms) / 1000;
260         format!(
261             "display_off={}s sleep={}s command={:?} idle={}s inhibited={} displays_off={} sleeping={}\n",
262             self.display_off_ms / 1000,
263             self.sleep_ms / 1000,
264             self.sleep_command(),
265             idle_s,
266             self.inhibited,
267             self.displays_off,
268             self.sleeping
269         )
270     }
271 
272     /// `ccectl idle …` — see `cce_ctl.rs` for the surface.
273     pub unsafe fn ipc(&mut self, args: &[&str]) -> String {
274         match args {
275             [] | ["status"] => self.status(),
276             ["wake"] => {
277                 self.on_activity();
278                 "ok\n".to_string()
279             }
280             ["display", "off"] => {
281                 self.set_displays(false);
282                 "ok\n".to_string()
283             }
284             ["display", "on"] => {
285                 self.set_displays(true);
286                 "ok\n".to_string()
287             }
288             ["sleep"] => {
289                 self.sleep_now();
290                 "ok\n".to_string()
291             }
292             ["timeouts", display, sleep] => {
293                 match (display.parse::<i64>(), sleep.parse::<i64>()) {
294                     (Ok(d), Ok(s)) if d >= 0 && s >= 0 => {
295                         let cfg = IdleConfig { display_off_s: d, sleep_s: s, sleep_command: self.sleep_command.clone() };
296                         self.configure(&cfg);
297                         "ok\n".to_string()
298                     }
299                     _ => "error: idle timeouts <display_off_s> <sleep_s> (non-negative seconds, 0 = off)\n".to_string(),
300                 }
301             }
302             _ => "error: usage: idle [status|wake|display on|display off|sleep|timeouts <display_off_s> <sleep_s>]\n".to_string(),
303         }
304     }
305 }
306 
307 unsafe extern "C" fn handle_display_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
308     let idle = &mut *(data as *mut IdleManager);
309     if !idle.inhibited && !idle.displays_off {
310         log::info!("idle: display timeout reached");
311         idle.set_displays(false);
312     }
313     0
314 }
315 
316 unsafe extern "C" fn handle_sleep_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
317     let idle = &mut *(data as *mut IdleManager);
318     if !idle.inhibited && !idle.sleeping {
319         log::info!("idle: sleep timeout reached");
320         idle.sleep_now();
321     }
322     0
323 }
324 
325 unsafe extern "C" fn handle_session_active(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
326     let idle = &mut *crate::container_of!(listener, IdleManager, session_active);
327     let session = (*idle.server).session;
328     if session.is_null() || !ffi::river_wlr_session_get_active(session) {
329         return;
330     }
331     log::info!("idle: session active (resume / VT switch), waking");
332     idle.on_activity();
333 }