nosleep_nix/
lib.rs

1//! Wrapper utility to block and unblock the Linux power save mode.
2//! It uses either the org.gnome.SessionManager D-Bus or the
3//! org.freedesktop.PowerManagement API under the hood.
4//!
5//! Heavily inspired on the Chromium source code:
6//! https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/services/device/wake_lock/power_save_blocker/power_save_blocker_linux.cc
7
8use dbus::blocking::{BlockingSender, Connection};
9use nosleep_types::NoSleepType;
10use snafu::{prelude::*, Backtrace};
11
12#[derive(Debug, Snafu)]
13pub enum Error {
14    #[snafu(display("General D-Bus Error"))]
15    DBus {
16        source: dbus::Error,
17        backtrace: Backtrace,
18    },
19
20    #[snafu(display("Invalid response from D-Bus"))]
21    InvalidResponse { backtrace: Backtrace },
22}
23
24pub type Result<T, E = Error> = std::result::Result<T, E>;
25
26#[derive(Debug, Copy, Clone)]
27enum DBusAPI {
28    GnomeApi,                  // org.gnome.Sessionmanager
29    FreeDesktopPowerApi,       // org.freedesktop.PowerMansagement
30    FreeDesktopScreenSaverAPI, // org.freedesktop.ScreenSaver
31}
32
33// Inhibit flags defined in the org.gnome.SessionManager interface.
34enum GnomeAPIInhibitFlags {
35    InhibitSuspendSession = 4,
36    InhibitMarkSessionIdle = 8,
37}
38
39struct NoSleepHandleCookie {
40    // Handle to a locks being held
41    handle: u32,
42    // The API used to acquire the lock
43    api: DBusAPI,
44}
45
46/// Returned by [`NoSleep::start`] to handle
47/// the power save block
48struct NoSleepHandle {
49    // All the locks that needs cleanup
50    cookies: Vec<NoSleepHandleCookie>,
51}
52
53impl NoSleepHandle {
54    /// Stop blocking the system from entering power save mode
55    pub fn stop(&self, d_bus: &Connection) -> Result<()> {
56        for cookie in &self.cookies {
57            let msg = uninhibit_msg(&cookie.api, cookie.handle);
58            d_bus
59                .send_with_reply_and_block(msg, std::time::Duration::from_millis(5000))
60                .context(DBusSnafu)?;
61        }
62        Ok(())
63    }
64}
65
66pub struct NoSleep {
67    // Connection to the D-Bus
68    d_bus: Connection,
69
70    // The unblock handle
71    no_sleep_handle: Option<NoSleepHandle>,
72}
73
74impl NoSleep {
75    /// Creates a new NoSleep type and connects to the D-Bus.
76    /// The session is automatically closed when the instance is dropped.
77    pub fn new() -> Result<NoSleep> {
78        Ok(NoSleep {
79            d_bus: Connection::new_session().context(DBusSnafu)?,
80            no_sleep_handle: None,
81        })
82    }
83
84    /// Blocks the system from entering low-power (sleep) mode.
85    /// By making an synchronous call to the D-Bus.
86    /// If [`self::stop`] is not called, then he lock will be cleaned up
87    /// when the bus is closed.
88    pub fn start(&mut self, nosleep_type: NoSleepType) -> Result<()> {
89        // Clear any previous handles held
90        self.stop()?;
91
92        let response = self.inhibit(&DBusAPI::GnomeApi, &nosleep_type);
93        if let Ok(cookie) = response {
94            self.no_sleep_handle = Some(NoSleepHandle {
95                cookies: vec![cookie],
96            });
97            return Ok(());
98        }
99        // Try again using the FreeDesktopPowerApi (we need two calls)
100        let mut cookies: Vec<NoSleepHandleCookie> = vec![];
101        if nosleep_type == NoSleepType::PreventUserIdleDisplaySleep {
102            let cookie = self.inhibit(&DBusAPI::FreeDesktopScreenSaverAPI, &nosleep_type)?;
103            cookies.push(cookie);
104        }
105        // Prevent suspension
106        let cookie = self.inhibit(&DBusAPI::FreeDesktopPowerApi, &nosleep_type)?;
107        cookies.push(cookie);
108        self.no_sleep_handle = Some(NoSleepHandle { cookies });
109        Ok(())
110    }
111
112    /// Stop blocking the system from entering power save mode
113    pub fn stop(&self) -> Result<()> {
114        if let Some(handle) = &self.no_sleep_handle {
115            return handle.stop(&self.d_bus);
116        }
117        Ok(())
118    }
119
120    fn inhibit(&self, api: &DBusAPI, nosleep_type: &NoSleepType) -> Result<NoSleepHandleCookie> {
121        let msg = inhibit_msg(api, nosleep_type);
122        let response = self
123            .d_bus
124            .send_with_reply_and_block(msg, std::time::Duration::from_millis(5000))
125            .context(DBusSnafu)?;
126        let handle = response.get1().context(InvalidResponseSnafu)?;
127        Ok(NoSleepHandleCookie { handle, api: *api })
128    }
129}
130
131fn inhibit_msg(api: &DBusAPI, nosleep_type: &NoSleepType) -> dbus::Message {
132    match api {
133        DBusAPI::GnomeApi => {
134            // Arguments are
135            // app_id:       application identifier
136            // toplevel_xid: toplevel x window identifier
137            // reason:       human readable reason
138            // flags:        flags that specify what should be inhibited
139            let flags = match nosleep_type {
140                NoSleepType::PreventUserIdleDisplaySleep => {
141                    GnomeAPIInhibitFlags::InhibitMarkSessionIdle as u32
142                        | GnomeAPIInhibitFlags::InhibitSuspendSession as u32
143                }
144                NoSleepType::PreventUserIdleSystemSleep => {
145                    GnomeAPIInhibitFlags::InhibitSuspendSession as u32
146                }
147            };
148            dbus::Message::call_with_args(
149                "org.gnome.SessionManager",
150                "/org/gnome/SessionManager",
151                "org.gnome.SessionManager",
152                "Inhibit",
153                (
154                    "org.powersaveblocker.app",
155                    0u32,
156                    "Power Save Blocker",
157                    flags,
158                ),
159            )
160        }
161        // The arguments of the method are:
162        //  app_id: The application identifier
163        //  reason: The reason for the inhibit
164        DBusAPI::FreeDesktopPowerApi => dbus::Message::call_with_args(
165            "org.freedesktop.PowerManagement",
166            "/org/freedesktop/PowerManagement/Inhibit",
167            "org.freedesktop.PowerManagement.Inhibit",
168            "Inhibit",
169            ("org.powersaveblocker.app", "Power Save Blocker"),
170        ),
171        DBusAPI::FreeDesktopScreenSaverAPI => dbus::Message::call_with_args(
172            "org.freedesktop.ScreenSaver",
173            "/org/freedesktop/ScreenSaver",
174            "org.freedesktop.ScreenSaver",
175            "Inhibit",
176            ("org.powersaveblocker.app", "Power Save Blocker"),
177        ),
178    }
179}
180
181fn uninhibit_msg(api: &DBusAPI, handle: u32) -> dbus::Message {
182    match api {
183        DBusAPI::GnomeApi => {
184            // Arguments are
185            // cookie:       lock from the inhibit method
186            dbus::Message::call_with_args(
187                "org.gnome.SessionManager",
188                "/org/gnome/SessionManager",
189                "org.gnome.SessionManager",
190                "Uninhibit",
191                (handle,),
192            )
193        }
194        DBusAPI::FreeDesktopPowerApi => dbus::Message::call_with_args(
195            "org.freedesktop.PowerManagement",
196            "/org/freedesktop/PowerManagement/Inhibit",
197            "org.freedesktop.PowerManagement.Inhibit",
198            "UnInhibit",
199            (handle,),
200        ),
201        DBusAPI::FreeDesktopScreenSaverAPI => dbus::Message::call_with_args(
202            "org.freedesktop.ScreenSaver",
203            "/org/freedesktop/ScreenSaver",
204            "org.freedesktop.ScreenSaver",
205            "UnInhibit",
206            (handle,),
207        ),
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use crate::*;
214
215    #[test]
216    fn test_inhibit_gnome_api_message_prevent_display_sleep() {
217        let msg = inhibit_msg(
218            &DBusAPI::GnomeApi,
219            &NoSleepType::PreventUserIdleDisplaySleep,
220        );
221        assert_eq!("/org/gnome/SessionManager", &*msg.path().unwrap());
222        assert_eq!("org.gnome.SessionManager", &*msg.interface().unwrap());
223        assert_eq!("Inhibit", &*msg.member().unwrap());
224        assert_eq!("org.gnome.SessionManager", &*msg.destination().unwrap());
225        assert_eq!(12, msg.get_items().last().unwrap().inner::<u32>().unwrap());
226    }
227
228    #[test]
229    fn test_inhibit_gnome_api_message_prevent_system_sleep() {
230        let msg = inhibit_msg(&DBusAPI::GnomeApi, &NoSleepType::PreventUserIdleSystemSleep);
231        assert_eq!("/org/gnome/SessionManager", &*msg.path().unwrap());
232        assert_eq!("org.gnome.SessionManager", &*msg.interface().unwrap());
233        assert_eq!("Inhibit", &*msg.member().unwrap());
234        assert_eq!("org.gnome.SessionManager", &*msg.destination().unwrap());
235        assert_eq!(4, msg.get_items().last().unwrap().inner::<u32>().unwrap());
236    }
237
238    #[test]
239    fn test_uninhibit_gnome_api() {
240        let msg = uninhibit_msg(&DBusAPI::GnomeApi, 0);
241        assert_eq!("/org/gnome/SessionManager", &*msg.path().unwrap());
242        assert_eq!("org.gnome.SessionManager", &*msg.interface().unwrap());
243        assert_eq!("Uninhibit", &*msg.member().unwrap());
244        assert_eq!("org.gnome.SessionManager", &*msg.destination().unwrap());
245        assert_eq!(0, msg.get_items().last().unwrap().inner::<u32>().unwrap());
246    }
247
248    #[test]
249    fn test_inhibit_freedesktop_screen_saver_api() {
250        let msg = inhibit_msg(
251            &DBusAPI::FreeDesktopScreenSaverAPI,
252            &NoSleepType::PreventUserIdleDisplaySleep,
253        );
254        assert_eq!("/org/freedesktop/ScreenSaver", &*msg.path().unwrap());
255        assert_eq!("org.freedesktop.ScreenSaver", &*msg.interface().unwrap());
256        assert_eq!("Inhibit", &*msg.member().unwrap());
257        assert_eq!("org.freedesktop.ScreenSaver", &*msg.destination().unwrap());
258    }
259
260    #[test]
261    fn test_uninhibit_freedesktop_screen_saver_api() {
262        let msg = uninhibit_msg(&DBusAPI::FreeDesktopScreenSaverAPI, 0);
263        assert_eq!("/org/freedesktop/ScreenSaver", &*msg.path().unwrap());
264        assert_eq!("org.freedesktop.ScreenSaver", &*msg.interface().unwrap());
265        assert_eq!("UnInhibit", &*msg.member().unwrap());
266        assert_eq!("org.freedesktop.ScreenSaver", &*msg.destination().unwrap());
267        assert_eq!(0, msg.get_items().last().unwrap().inner::<u32>().unwrap());
268    }
269
270    #[test]
271    fn test_freedesktop_power_api() {
272        let msg = inhibit_msg(
273            &DBusAPI::FreeDesktopPowerApi,
274            &NoSleepType::PreventUserIdleSystemSleep,
275        );
276        assert_eq!(
277            "/org/freedesktop/PowerManagement/Inhibit",
278            &*msg.path().unwrap()
279        );
280        assert_eq!(
281            "org.freedesktop.PowerManagement.Inhibit",
282            &*msg.interface().unwrap()
283        );
284        assert_eq!("Inhibit", &*msg.member().unwrap());
285        assert_eq!(
286            "org.freedesktop.PowerManagement",
287            &*msg.destination().unwrap()
288        );
289    }
290
291    // Can only run with an active Gnome Session
292    #[test]
293    #[ignore]
294    fn test_start() {
295        let mut nosleep = NoSleep::new().unwrap();
296        nosleep
297            .start(NoSleepType::PreventUserIdleSystemSleep)
298            .unwrap();
299        std::thread::sleep(std::time::Duration::from_millis(2000));
300        nosleep.stop().unwrap();
301        std::thread::sleep(std::time::Duration::from_millis(2000));
302    }
303}