Skip to main content

screen_app/recp/
keep_awake.rs

1//! M-RECP.3 / AUT-264 — Display-keep-awake RAII guard.
2//!
3//! `KeepAwakeGuard` holds an OS-level assertion that prevents the
4//! display from dimming / sleeping while the preview is active. On
5//! drop the assertion releases automatically (RAII). Production
6//! macOS implementation via `IOPMAssertion`, Windows via
7//! `SetThreadExecutionState`, Linux via `org.freedesktop.ScreenSaver`
8//! D-Bus inhibit — all **deferred** to a follow-up commit that
9//! needs hardware verification.
10//!
11//! Today's implementation: a counter-only stub so the state-machine
12//! tests verify the RAII contract on every OS. The
13//! `PreviewSession::new_with_keep_awake` integration lives in
14//! `crates/app/src/preview.rs` once the OS calls land.
15
16use std::sync::atomic::{AtomicU32, Ordering};
17
18/// Process-wide active-assertion count. Real OS assertions don't
19/// stack but our stub does so the tests can assert on RAII drop.
20static ACTIVE_COUNT: AtomicU32 = AtomicU32::new(0);
21
22/// Snapshot of active keep-awake assertions across the process.
23/// Useful for assertions in tests + for the `cleanup_smoke` test in
24/// M-RECP.4 (no zombie assertions after app quit).
25#[must_use]
26pub fn active_assertions() -> u32 {
27    ACTIVE_COUNT.load(Ordering::SeqCst)
28}
29
30/// RAII guard that holds the keep-awake assertion while alive.
31///
32/// Construct via [`KeepAwakeGuard::new`]; drop to release.
33#[derive(Debug)]
34pub struct KeepAwakeGuard {
35    /// `true` once the OS assertion has been claimed. `false` after
36    /// `release`. Drop-of-released is a no-op so `release` can be
37    /// called explicitly.
38    active: bool,
39}
40
41impl KeepAwakeGuard {
42    /// Acquire a new keep-awake assertion. The real OS impl will fail
43    /// if the assertion can't be created (rare); the stub today
44    /// always succeeds.
45    pub fn new() -> Self {
46        ACTIVE_COUNT.fetch_add(1, Ordering::SeqCst);
47        // M-RECP.3 OS calls land here:
48        //   macOS: IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, ...)
49        //   windows-rs: SetThreadExecutionState(ES_DISPLAY_REQUIRED | ES_CONTINUOUS)
50        //   linux: ScreenSaver::Inhibit via zbus.
51        Self { active: true }
52    }
53
54    /// Release the assertion explicitly (before Drop). Idempotent —
55    /// repeated calls are safe.
56    pub fn release(&mut self) {
57        if self.active {
58            ACTIVE_COUNT.fetch_sub(1, Ordering::SeqCst);
59            self.active = false;
60        }
61    }
62}
63
64impl Default for KeepAwakeGuard {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl Drop for KeepAwakeGuard {
71    fn drop(&mut self) {
72        self.release();
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::sync::Mutex;
80
81    // Serialise these tests since they touch a process-wide static.
82    static SERIAL: Mutex<()> = Mutex::new(());
83
84    #[test]
85    fn new_increments_count() {
86        let _guard = SERIAL
87            .lock()
88            .unwrap_or_else(std::sync::PoisonError::into_inner);
89        let baseline = active_assertions();
90        let _g = KeepAwakeGuard::new();
91        assert_eq!(active_assertions(), baseline + 1);
92    }
93
94    #[test]
95    fn drop_decrements_count() {
96        let _guard = SERIAL
97            .lock()
98            .unwrap_or_else(std::sync::PoisonError::into_inner);
99        let baseline = active_assertions();
100        {
101            let _g = KeepAwakeGuard::new();
102            assert_eq!(active_assertions(), baseline + 1);
103        }
104        assert_eq!(active_assertions(), baseline);
105    }
106
107    #[test]
108    fn explicit_release_is_idempotent() {
109        let _guard = SERIAL
110            .lock()
111            .unwrap_or_else(std::sync::PoisonError::into_inner);
112        let baseline = active_assertions();
113        let mut g = KeepAwakeGuard::new();
114        assert_eq!(active_assertions(), baseline + 1);
115        g.release();
116        assert_eq!(active_assertions(), baseline);
117        g.release(); // no-op
118        assert_eq!(active_assertions(), baseline);
119        // Drop after explicit release does NOT double-decrement.
120        drop(g);
121        assert_eq!(active_assertions(), baseline);
122    }
123}