1use crate::Result;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum Awake {
20 Busy,
23 System,
25 Display,
27}
28
29pub struct KeepAwake {
31 _inner: imp::Guard,
32 _not_send: std::marker::PhantomData<*const ()>,
34}
35
36impl KeepAwake {
37 pub fn new(kind: Awake, reason: &str) -> Result<Self> {
40 let reason: String = reason.chars().filter(|c| !c.is_control()).take(128).collect();
41 let reason = if reason.trim().is_empty() { "working".to_string() } else { reason };
42 Ok(Self { _inner: imp::Guard::new(kind, &reason)?, _not_send: std::marker::PhantomData })
43 }
44}
45
46impl std::fmt::Debug for KeepAwake {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.write_str("KeepAwake")
49 }
50}
51
52#[cfg(target_os = "macos")]
53#[allow(unsafe_code)]
54mod imp {
55 use objc2::rc::Retained;
56 use objc2::runtime::{NSObjectProtocol, ProtocolObject};
57 use objc2_foundation::{NSActivityOptions, NSProcessInfo, NSString};
58
59 use super::*;
60
61 pub struct Guard(Retained<ProtocolObject<dyn NSObjectProtocol>>);
62
63 impl Guard {
64 pub fn new(kind: Awake, reason: &str) -> Result<Self> {
65 let options = match kind {
66 Awake::Busy => NSActivityOptions::UserInitiatedAllowingIdleSystemSleep | NSActivityOptions::LatencyCritical,
67 Awake::System => NSActivityOptions::UserInitiated,
68 Awake::Display => NSActivityOptions::UserInitiated | NSActivityOptions::IdleDisplaySleepDisabled,
69 };
70 let token = NSProcessInfo::processInfo().beginActivityWithOptions_reason(options, &NSString::from_str(reason));
71 Ok(Self(token))
72 }
73 }
74
75 impl Drop for Guard {
76 fn drop(&mut self) {
77 unsafe { NSProcessInfo::processInfo().endActivity(&self.0) };
80 }
81 }
82}
83
84#[cfg(target_os = "windows")]
85#[allow(unsafe_code)]
86mod imp {
87 use std::cell::RefCell;
88
89 use crate::NativeError;
90 use windows::Win32::System::Power::{ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, EXECUTION_STATE, SetThreadExecutionState};
91
92 use super::*;
93
94 thread_local! {
95 static ACTIVE: RefCell<(u64, Vec<(u64, EXECUTION_STATE)>)> = const { RefCell::new((0, Vec::new())) };
97 }
98
99 fn apply(active: &[(u64, EXECUTION_STATE)]) -> bool {
100 let flags = active.iter().fold(ES_CONTINUOUS, |acc, (_, f)| acc | *f);
101 let previous = unsafe { SetThreadExecutionState(flags) };
104 previous.0 != 0
105 }
106
107 pub struct Guard(Option<u64>);
108
109 impl Guard {
110 pub fn new(kind: Awake, _reason: &str) -> Result<Self> {
111 let flags = match kind {
112 Awake::Busy => return Ok(Self(None)),
113 Awake::System => ES_SYSTEM_REQUIRED,
114 Awake::Display => ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED,
115 };
116 ACTIVE.with(|a| {
117 let mut a = a.borrow_mut();
118 a.0 = a.0.wrapping_add(1);
119 let id = a.0;
120 a.1.push((id, flags));
121 if apply(&a.1) {
122 Ok(Self(Some(id)))
123 } else {
124 a.1.retain(|(i, _)| *i != id);
125 Err(NativeError::Os("Windows refused the power request".into()))
126 }
127 })
128 }
129 }
130
131 impl Drop for Guard {
132 fn drop(&mut self) {
133 let Some(id) = self.0 else { return };
134 let _ = ACTIVE.try_with(|a| {
135 if let Ok(mut a) = a.try_borrow_mut() {
136 a.1.retain(|(i, _)| *i != id);
137 apply(&a.1);
138 }
139 });
140 }
141 }
142}
143
144#[cfg(target_os = "linux")]
145mod imp {
146 use super::*;
147 use crate::NativeError;
148
149 pub struct Guard {
151 _fd: Option<zbus::zvariant::OwnedFd>,
152 }
153
154 impl Guard {
155 pub fn new(kind: Awake, reason: &str) -> Result<Self> {
156 if kind == Awake::Busy {
157 return Ok(Self { _fd: None });
158 }
159 let os = |e: zbus::Error| NativeError::Os(format!("logind refused the inhibitor ({})", crate::os_text(e)));
160 let conn = zbus::blocking::Connection::system().map_err(os)?;
161 let who =
162 std::env::current_exe().ok().and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())).unwrap_or_else(|| "app".into());
163 let reply = conn
164 .call_method(
165 Some("org.freedesktop.login1"),
166 "/org/freedesktop/login1",
167 Some("org.freedesktop.login1.Manager"),
168 "Inhibit",
169 &("idle:sleep", who.as_str(), reason, "block"),
170 )
171 .map_err(os)?;
172 let fd: zbus::zvariant::OwnedFd = reply.body().deserialize().map_err(os)?;
173 Ok(Self { _fd: Some(fd) })
174 }
175 }
176}
177
178#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
179mod imp {
180 use super::*;
181 use crate::NativeError;
182
183 pub struct Guard;
184
185 impl Guard {
186 pub fn new(_kind: Awake, _reason: &str) -> Result<Self> {
187 Err(NativeError::Unsupported)
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::NativeError;
196
197 #[test]
198 fn busy_is_held_and_released_without_side_effects() {
199 let r = KeepAwake::new(Awake::Busy, "ashlar test\nwith a newline");
201 if cfg!(any(target_os = "macos", target_os = "windows", target_os = "linux")) {
202 let g = r.unwrap();
203 drop(g);
204 } else {
205 assert_eq!(r.err(), Some(NativeError::Unsupported));
206 }
207 }
208
209 #[cfg(target_os = "macos")]
210 #[test]
211 fn the_system_assertion_shows_up_while_the_guard_lives() {
212 let reason = format!("ashlar-native test {}", std::process::id());
213 let assertions = || {
214 String::from_utf8_lossy(&std::process::Command::new("pmset").args(["-g", "assertions"]).output().map(|o| o.stdout).unwrap_or_default())
215 .into_owned()
216 };
217 let g = KeepAwake::new(Awake::System, &reason).unwrap();
218 let during = assertions();
219 drop(g);
220 let after = assertions();
221 if during.is_empty() {
222 return; }
224 assert!(during.contains(&reason), "{during}");
225 assert!(!after.contains(&reason), "{after}");
226 }
227}