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 }