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}