Skip to main content

sioul_sync/
power.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! What this computer says of itself, for work it may do while you are away
5//! (the spam filter's training by itself: docs/spam-filter.md, "Training by
6//! itself"): on mains power or on its battery, saving power or not, idle or
7//! in use, its connection metered or not. Each fact is `None` when this
8//! computer cannot tell it (no such service, no answer in time, a system not
9//! read here): whoever decides counts an unknown fact as not holding.
10//!
11//! Also whether a person is at it now (`kept_session`, `present_of`): input
12//! within ten minutes, its session not locked. A computer says so in its entry
13//! among your devices, and while you sleep a phone's background steps slow
14//! down when nobody is at any computer (docs/android.md, "In the background").
15//!
16//! All of it is read on one thread of its own (`watch`), every thirty
17//! seconds while it is asked for, on one connection per bus kept for the
18//! process (made again only once the bus dropped it), and kept: whoever asks
19//! reads the kept values (`read`, `supply`, `kept_power`, `kept_session`) and
20//! never opens a bus nor asks it anything. A bus that stops answering after
21//! the thread connected only gives its questions up, a second each; one that
22//! takes a new connection and never answers is given up after `bus::SETUP`
23//! and holds one thread of its own, never this one nor libdbus's
24//! process-wide locks (the connections are `crate::bus`'s).
25//!
26//! - **Linux and the BSDs**, over D-Bus: UPower's `OnBattery`;
27//!   power-profiles-daemon's `ActiveProfile`, under both of its names
28//!   (`org.freedesktop.UPower.PowerProfiles`, the older
29//!   `net.hadess.PowerProfiles`); the session's idle time (the desktop's
30//!   `org.freedesktop.ScreenSaver`, in milliseconds as KDE gives it, GNOME's
31//!   `org.gnome.Mutter.IdleMonitor`) and whether it is locked (both screen
32//!   savers, logind's `LockedHint`; logind's `IdleHint` only as a word for
33//!   idle, since some desktops never set it); NetworkManager's `Metered`.
34//! - **Windows**: `GetSystemPowerStatus` (its AC line, its battery saver),
35//!   `GetLastInputInfo`, and whether the session is locked
36//!   (`WTSQuerySessionInformationW`'s `WTSSessionInfoEx`). The connection's
37//!   cost is not read: unknown.
38//! - **macOS**: IOKit's power sources, Low Power Mode (as `pmset -g` says
39//!   it), `CGEventSourceSecondsSinceLastEventType`, and whether the screen is
40//!   locked (`CGSessionCopyCurrentDictionary`'s `CGSSessionScreenIsLocked`).
41//!   The connection's cost is not read: unknown.
42//! - **Android**: nothing; a phone never trains, and its own entry says it
43//!   is in use while Sioul is on its screen.
44
45use std::sync::{Condvar, Mutex, Once};
46use std::time::{Duration, Instant};
47
48/// What this computer says of itself now.
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50pub struct Power {
51    /// On mains power, not on its battery.
52    pub on_mains: Option<bool>,
53    /// Saving power: a power-saver profile, Windows' battery saver, Low Power Mode.
54    pub power_saver: Option<bool>,
55    /// Nobody's hands on it for `IDLE` at least, or its session locked.
56    pub idle: Option<bool>,
57    /// Its connection metered (NetworkManager: yes, or guessed yes).
58    pub metered: Option<bool>,
59}
60
61impl Power {
62    /// Its power supply allows long work: on mains power, and not saving
63    /// power, both known.
64    pub fn supplied(&self) -> bool {
65        self.on_mains == Some(true) && self.power_saver == Some(false)
66    }
67}
68
69/// How long without anyone's hands on the computer counts as idle.
70pub const IDLE: Duration = Duration::from_secs(15 * 60);
71
72/// How long one question to the system may take.
73#[cfg_attr(not(all(unix, not(any(target_os = "macos", target_os = "android")))), allow(dead_code))]
74const ASK: Duration = Duration::from_secs(1);
75
76/// Every fact, as the reading thread read it (`watch`, started and asked):
77/// its last reading while younger than `KEPT_FOR`, else the next one, waited
78/// for `WAIT` at most (the first of this process, or the thread asked again
79/// after a rest: `ASKED_FOR`); none by then, nothing said. Never opens a bus
80/// nor asks it anything itself; for work that may wait on a thread of its
81/// own (the spam filter's training), never for an exchange (`kept_power`).
82pub fn read() -> Power {
83    if !imp::READS {
84        return Power::default();
85    }
86    watch();
87    fresh_within(&SHARED, &BELL, WAIT).map(|reading| reading.power).unwrap_or_default()
88}
89
90/// Its power supply alone (mains, saving power), as `read` gives it: what a
91/// training running asks every half minute.
92pub fn supply() -> Power {
93    let power = read();
94    Power { on_mains: power.on_mains, power_saver: power.power_saver, ..Power::default() }
95}
96
97/// NetworkManager's `Metered` (`NMMetered`): yes or guessed yes, metered; no
98/// or guessed no, not; unknown (0) or anything else, unknown.
99pub fn metered_of(value: u32) -> Option<bool> {
100    match value {
101        1 | 3 => Some(true),
102        2 | 4 => Some(false),
103        _ => None,
104    }
105}
106
107/// Idle, from what the session said: locked, idle; else the shortest time
108/// without input any source gave (the newest touch wins); nothing said, unknown.
109pub fn idle_of(locked: bool, shortest: Option<Duration>) -> Option<bool> {
110    if locked {
111        return Some(true);
112    }
113    shortest.map(|d| d >= IDLE)
114}
115
116/// What the session says of the person at this computer now.
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub struct Session {
119    /// How long since the last input (keyboard, pointer, touch): the shortest
120    /// any source gave (the newest touch wins); none when no source said.
121    pub since_input: Option<Duration>,
122    /// Its session locked (a screen locker on): a source says so, and none
123    /// says it is not.
124    pub locked: bool,
125    /// Its sources disagree on the lock (one says locked, another not: a
126    /// stale hint of logind's, another session's, GNOME's shield up with no
127    /// lock behind it): the lock is not taken, input alone says whether
128    /// someone is at it (`present_of`).
129    pub disputed: bool,
130}
131
132/// How long without input a person no longer counts as at this computer (`present_of`).
133pub const PRESENT: Duration = Duration::from_secs(10 * 60);
134
135/// Whether a person is at this computer now: its session locked, no; else a
136/// press in Sioul (its window, do-not-disturb's switch) less than `PRESENT`
137/// ago (`pressed`), yes, whatever the system says of input; else input less
138/// than `PRESENT` ago, as the session says, also when its sources disagree on
139/// the lock (`Session::disputed`: input alone says it); nothing said of
140/// input, unknown. Unknown is never "nobody": Sioul running then counts as
141/// someone at it, as before (`devices::others_in_use`).
142pub fn present_of(session: Session, pressed: Option<Duration>) -> Option<bool> {
143    if session.locked {
144        return Some(false);
145    }
146    if pressed.is_some_and(|ago| ago < PRESENT) {
147        return Some(true);
148    }
149    session.since_input.map(|since| since < PRESENT)
150}
151
152/// `present_of`, from the session as last read (`kept_session`: the reading
153/// and how long ago it was made; none when there is none fresh enough) and
154/// the last press in Sioul (how long ago): a press made since the reading
155/// counts whatever the reading said, its lock included (someone unlocked the
156/// computer to make it); an older one as `present_of` reads it. No reading, a
157/// press within `PRESENT` says someone, and nothing else says anything
158/// (unknown: Sioul running counts).
159pub fn present_since(reading: Option<(Session, Duration)>, pressed: Option<Duration>) -> Option<bool> {
160    let recent = pressed.filter(|ago| *ago < PRESENT);
161    match reading {
162        Some((_, read)) if recent.is_some_and(|ago| ago <= read) => Some(true),
163        Some((session, _)) => present_of(session, pressed),
164        None => recent.map(|_| true),
165    }
166}
167
168/// How often the reading thread reads everything while it is asked for (`watch`).
169pub const EVERY: Duration = Duration::from_secs(30);
170/// How long `read` waits for a fresh reading: a few questions to the system,
171/// a second each at most, and the connections' setup.
172pub const WAIT: Duration = Duration::from_secs(10);
173/// How long a reading is kept for (`kept_session`, `kept_power`, `read`):
174/// past it, none (the thread held by a bus, or resting), and Sioul running
175/// counts, as when nothing is said.
176pub const KEPT_FOR: Duration = Duration::from_secs(120);
177/// How long the reading thread goes on after it was last asked (`watch`,
178/// `read`); past it, it rests until the next ask. The window asks at each
179/// exchange, every minute; a process that asked once (`sioul watch`, whose
180/// training asks only when nothing else holds it back) is not read every
181/// half minute for days after.
182pub const ASKED_FOR: Duration = Duration::from_secs(15 * 60);
183
184/// One reading of the thread: when it began (a press in Sioul made while it
185/// was read is newer than it: `present_since`), the session, the power
186/// supply and the connection's cost.
187#[derive(Debug, Clone, Copy)]
188struct Reading {
189    began: Instant,
190    session: Session,
191    power: Power,
192}
193
194/// What the reading thread and those who ask share: its last reading, and
195/// when it was last asked for readings.
196struct Shared {
197    reading: Option<Reading>,
198    asked: Option<Instant>,
199}
200
201static SHARED: Mutex<Shared> = Mutex::new(Shared { reading: None, asked: None });
202/// Rung at each reading kept and at each ask: whoever waits on it looks again.
203static BELL: Condvar = Condvar::new();
204/// The reading thread started (`start`).
205static STARTED: Once = Once::new();
206
207/// A reading by `read`, timed from when it began (`began`), not from when it
208/// came back: a press made while a slow reading said locked is newer than it
209/// (review of battery part C, G6).
210fn reading_of(began: Instant, read: impl FnOnce() -> (Session, Power)) -> Reading {
211    let (session, power) = read();
212    Reading { began, session, power }
213}
214
215/// Starts, once in this process, the thread that reads everything on its own
216/// connections to this process's buses (`imp::Reader`) every `EVERY` while
217/// asked within `ASKED_FOR`, and keeps the last reading; and asks it.
218/// Nothing else in the process asks the system for these: an exchange, a
219/// start, Sioul closing and the training read what is kept.
220pub fn watch() {
221    start(imp::Reader::here);
222    ask(&SHARED, &BELL, Instant::now());
223}
224
225/// `watch`, on the buses at these D-Bus addresses (none: that bus not read):
226/// stand-ins, so that a test never reads the session or the system of
227/// whoever runs it. Only the first start in a process counts; elsewhere than
228/// on Linux and the BSDs, nothing is read over D-Bus and they are not used.
229pub fn watch_on(session: Option<String>, system: Option<String>) {
230    start(move || imp::Reader::at(session, system));
231    ask(&SHARED, &BELL, Instant::now());
232}
233
234/// The reading thread, started once, its `imp::Reader` made on it by
235/// `reader`: a reading, kept and rung; `EVERY` on, the next, once someone
236/// asked within `ASKED_FOR` (`wanted`), else at the next ask. Nothing
237/// started where nothing is read (a phone).
238fn start(reader: impl FnOnce() -> imp::Reader + Send + 'static) {
239    if !imp::READS {
240        return;
241    }
242    STARTED.call_once(move || {
243        let _ = std::thread::Builder::new().name("sioul-system".into()).spawn(move || {
244            let mut reader = reader();
245            loop {
246                let reading = reading_of(Instant::now(), || reader.read());
247                let Ok(mut shared) = SHARED.lock() else { return };
248                shared.reading = Some(reading);
249                BELL.notify_all();
250                drop(shared);
251                std::thread::sleep(EVERY);
252                let Ok(shared) = SHARED.lock() else { return };
253                if BELL.wait_while(shared, |shared| !wanted(shared.asked, Instant::now())).is_err() {
254                    return;
255                }
256            }
257        });
258    });
259}
260
261/// Readings asked for at `now`: the thread resting wakes (`wanted`).
262fn ask(shared: &Mutex<Shared>, bell: &Condvar, now: Instant) {
263    if let Ok(mut shared) = shared.lock() {
264        shared.asked = Some(now);
265        bell.notify_all();
266    }
267}
268
269/// Whether the reading thread goes on reading at `now`: asked within `ASKED_FOR`.
270fn wanted(asked: Option<Instant>, now: Instant) -> bool {
271    asked.is_some_and(|at| now.saturating_duration_since(at) < ASKED_FOR)
272}
273
274/// `reading` while it is younger than `KEPT_FOR` at `now`.
275fn fresh(reading: Option<Reading>, now: Instant) -> Option<Reading> {
276    reading.filter(|reading| now.saturating_duration_since(reading.began) < KEPT_FOR)
277}
278
279/// The reading in `shared` once fresh (`fresh`), waited for `limit` at most,
280/// looked at again at each ring of `bell`; none after it.
281fn fresh_within(shared: &Mutex<Shared>, bell: &Condvar, limit: Duration) -> Option<Reading> {
282    let guard = shared.lock().ok()?;
283    let (guard, _) = bell.wait_timeout_while(guard, limit, |shared| fresh(shared.reading, Instant::now()).is_none()).ok()?;
284    fresh(guard.reading, Instant::now())
285}
286
287/// The session as last read (`watch`) and how long ago that reading began;
288/// none before the first, or once it is `KEPT_FOR` old (a bus that stopped
289/// answering, the thread resting). Never asks the system, never waits.
290pub fn kept_session() -> Option<(Session, Duration)> {
291    let now = Instant::now();
292    let reading = fresh(SHARED.lock().ok()?.reading, now)?;
293    Some((reading.session, now.saturating_duration_since(reading.began)))
294}
295
296/// The power supply and the connection's cost as last read (`watch`); none
297/// before the first reading, or once it is `KEPT_FOR` old. Never asks the
298/// system, never waits: for an exchange (the window's `metered`).
299pub fn kept_power() -> Option<Power> {
300    fresh(SHARED.lock().ok()?.reading, Instant::now()).map(|reading| reading.power)
301}
302
303/// Whether the reading thread kept a reading yet in this process, its first
304/// waited for `limit` at most while there is none (the window's first
305/// exchange, `metered`: a connection not read yet counts as metered).
306pub fn read_once_within(limit: Duration) -> bool {
307    ever_read_within(&SHARED, &BELL, limit)
308}
309
310/// `read_once_within`, on `shared` and its `bell`.
311fn ever_read_within(shared: &Mutex<Shared>, bell: &Condvar, limit: Duration) -> bool {
312    let Ok(guard) = shared.lock() else { return false };
313    bell.wait_timeout_while(guard, limit, |shared| shared.reading.is_none()).is_ok_and(|(guard, _)| guard.reading.is_some())
314}
315
316/// What a Linux or BSD desktop's services answered of its session
317/// (`imp::session`), each none when it did not answer: the desktop's screen
318/// saver (KDE's, and others of the freedesktop name), GNOME's idle monitor
319/// and screen shield, logind's hints.
320#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
321#[cfg_attr(not(all(unix, not(any(target_os = "macos", target_os = "android")))), allow(dead_code))]
322struct LinuxAnswers {
323    /// `org.freedesktop.ScreenSaver.GetActive`.
324    saver_active: Option<bool>,
325    /// `org.freedesktop.ScreenSaver.GetSessionIdleTime`: milliseconds, as KDE
326    /// gives them (a desktop giving seconds reads as never idle, never as idle too soon).
327    saver_idle_ms: Option<u32>,
328    /// `org.gnome.Mutter.IdleMonitor.GetIdletime`, milliseconds.
329    mutter_idle_ms: Option<u64>,
330    /// `org.gnome.ScreenSaver.GetActive`.
331    gnome_active: Option<bool>,
332    /// logind's `LockedHint`.
333    locked_hint: Option<bool>,
334    /// logind's `IdleSinceHint` (microseconds of the real clock), read only
335    /// while its `IdleHint` holds: a word for idle only.
336    idle_since_us: Option<u64>,
337}
338
339/// The session as a Linux or BSD desktop's answers say it, `now` the real
340/// clock since the epoch: locked when a locker says so and none says
341/// otherwise, disputed when they disagree (logind's hint left locked by a
342/// locker that died, while the desktop's own screen saver says unlocked); the
343/// shortest time since input any of them gave.
344#[cfg_attr(not(all(unix, not(any(target_os = "macos", target_os = "android")))), allow(dead_code))]
345fn linux_session(answers: &LinuxAnswers, now: Duration) -> Session {
346    let lockers = [answers.saver_active, answers.gnome_active, answers.locked_hint];
347    let (locked, unlocked) = (lockers.contains(&Some(true)), lockers.contains(&Some(false)));
348    let times = [answers.saver_idle_ms.map(|ms| Duration::from_millis(u64::from(ms))), answers.mutter_idle_ms.map(Duration::from_millis), answers.idle_since_us.map(|us| now.saturating_sub(Duration::from_micros(us)))];
349    Session { since_input: times.into_iter().flatten().min(), locked: locked && !unlocked, disputed: locked && unlocked }
350}
351
352/// The session as Windows' answers say it: the tick counts now and at the
353/// last input (`GetTickCount`, `GetLastInputInfo`; none when it failed; both
354/// wrap alike every 49.7 days), and `WTSSessionInfoEx`'s answer (none when
355/// Windows gave none).
356#[cfg_attr(not(windows), allow(dead_code))]
357fn windows_session(ticks: Option<(u32, u32)>, wts: Option<&[u8]>) -> Session {
358    let since_input = ticks.map(|(now, input)| Duration::from_millis(u64::from(now.wrapping_sub(input))));
359    Session { since_input, locked: wts.and_then(windows_flags).and_then(windows_locked) == Some(true), disputed: false }
360}
361
362/// Windows' word on a session's lock (`WTSINFOEX_LEVEL1_W::SessionFlags`):
363/// `WTS_SESSIONSTATE_LOCK` (0), locked; `WTS_SESSIONSTATE_UNLOCK` (1), not;
364/// anything else (`WTS_SESSIONSTATE_UNKNOWN`), unknown.
365#[cfg_attr(not(windows), allow(dead_code))]
366fn windows_locked(flags: i32) -> Option<bool> {
367    match flags {
368        0 => Some(true),
369        1 => Some(false),
370        _ => None,
371    }
372}
373
374/// The flags of `WTSSessionInfoEx`'s answer, as Windows lays it out
375/// (`WTSINFOEXW`): its level (a `DWORD`, 1), then, 8-aligned for the times
376/// that follow, its session's id, state and flags (each 4 bytes): the flags
377/// at byte 16. None for another level, or an answer too short to hold them.
378#[cfg_attr(not(windows), allow(dead_code))]
379fn windows_flags(answer: &[u8]) -> Option<i32> {
380    let word = |at: usize| answer.get(at..at + 4).and_then(|b| <[u8; 4]>::try_from(b).ok());
381    let level = u32::from_le_bytes(word(0)?);
382    (level == 1).then(|| word(16).map(i32::from_le_bytes)).flatten()
383}
384
385/// The session as macOS says it: the seconds since its last input
386/// (`CGEventSourceSecondsSinceLastEventType`; none from a value that is no
387/// time), and whether its screen is locked.
388#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
389fn macos_session(seconds: f64, locked: bool) -> Session {
390    let since_input = if seconds.is_finite() && seconds >= 0.0 { Duration::try_from_secs_f64(seconds).ok() } else { None };
391    Session { since_input, locked, disputed: false }
392}
393
394#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
395mod imp {
396    use super::{ASK, LinuxAnswers, Power, Session, idle_of, linux_session, metered_of};
397    use dbus::blocking::Connection;
398    use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
399    use std::time::Duration;
400
401    /// Something is read here.
402    pub(super) const READS: bool = true;
403
404    /// What the reading thread keeps between its readings: where each bus is
405    /// (none: that bus not read), and the connection made to each, kept while
406    /// the bus keeps it: made again only once the bus dropped it, never after
407    /// a question given up (`held`).
408    pub(super) struct Reader {
409        session_at: Option<String>,
410        system_at: Option<String>,
411        session: Option<Connection>,
412        system: Option<Connection>,
413    }
414
415    impl Reader {
416        /// On the buses this process's environment names (`bus::session_address`, `bus::system_address`).
417        pub(super) fn here() -> Reader {
418            Reader::at(crate::bus::session_address(), Some(crate::bus::system_address()))
419        }
420
421        /// On the buses at these addresses; nothing connected yet.
422        pub(super) fn at(session: Option<String>, system: Option<String>) -> Reader {
423            Reader { session_at: session, system_at: system, session: None, system: None }
424        }
425
426        /// Everything, read now: the power supply and the connection's cost
427        /// on the system's bus, the session on both (`session_of`), a few
428        /// questions each given up after `ASK`.
429        pub(super) fn read(&mut self) -> (Session, Power) {
430            let system = held(&mut self.system, self.system_at.as_deref());
431            let mut power = Power { on_mains: system.and_then(on_mains), power_saver: system.and_then(power_saver), metered: system.and_then(metered), idle: None };
432            let bus = held(&mut self.session, self.session_at.as_deref());
433            let session = session_of(bus, system);
434            power.idle = idle_of(session.locked, session.since_input);
435            (session, power)
436        }
437    }
438
439    /// The connection kept in `slot` to the bus at `address`: what came on it
440    /// read first (`drained`: late answers, and the bus gone), then the one
441    /// kept while the bus keeps it, else a new one (`crate::bus::connect`,
442    /// `bus::SETUP` at most). None without such a bus, or while it cannot be
443    /// reached (tried again at the next reading).
444    fn held<'a>(slot: &'a mut Option<Connection>, address: Option<&str>) -> Option<&'a Connection> {
445        if let Some(connection) = slot.as_ref() {
446            drained(connection);
447        }
448        if slot.as_ref().is_some_and(|connection| !connection.channel().is_connected()) {
449            *slot = None;
450        }
451        if slot.is_none() {
452            *slot = address.and_then(|address| crate::bus::connect(address).ok()).map(Connection::from);
453        }
454        slot.as_ref()
455    }
456
457    /// What came on a kept connection and nobody waits for (an answer come
458    /// after its question was given up, the bus's own word on its name), read
459    /// and dropped, a few at most at each reading: a kept connection never
460    /// piles them up. A bus gone is learnt here (`held`).
461    fn drained(connection: &Connection) {
462        for _ in 0..32 {
463            if !connection.process(Duration::ZERO).unwrap_or(false) {
464                return;
465            }
466        }
467    }
468
469    fn on_mains(system: &Connection) -> Option<bool> {
470        let on_battery: bool = system.with_proxy("org.freedesktop.UPower", "/org/freedesktop/UPower", ASK).get("org.freedesktop.UPower", "OnBattery").ok()?;
471        Some(!on_battery)
472    }
473
474    /// power-profiles-daemon under its name, else under its older one.
475    fn power_saver(system: &Connection) -> Option<bool> {
476        [("org.freedesktop.UPower.PowerProfiles", "/org/freedesktop/UPower/PowerProfiles"), ("net.hadess.PowerProfiles", "/net/hadess/PowerProfiles")]
477            .into_iter()
478            .find_map(|(name, path)| system.with_proxy(name, path, ASK).get::<String>(name, "ActiveProfile").ok())
479            .map(|profile| profile == "power-saver")
480    }
481
482    fn metered(system: &Connection) -> Option<bool> {
483        let value: u32 = system.with_proxy("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", ASK).get("org.freedesktop.NetworkManager", "Metered").ok()?;
484        metered_of(value)
485    }
486
487    /// The session's screen savers and idle monitors (on its own bus), then
488    /// logind's word on it (on the system's), read as `linux_session` says.
489    fn session_of(bus: Option<&Connection>, system: Option<&Connection>) -> Session {
490        let mut answers = LinuxAnswers::default();
491        if let Some(session) = bus {
492            let saver = session.with_proxy("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", ASK);
493            let active: Result<(bool,), _> = saver.method_call("org.freedesktop.ScreenSaver", "GetActive", ());
494            answers.saver_active = active.ok().map(|(on,)| on);
495            let since: Result<(u32,), _> = saver.method_call("org.freedesktop.ScreenSaver", "GetSessionIdleTime", ());
496            answers.saver_idle_ms = since.ok().map(|(ms,)| ms);
497            let mutter: Result<(u64,), _> = session.with_proxy("org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", ASK).method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ());
498            answers.mutter_idle_ms = mutter.ok().map(|(ms,)| ms);
499            let gnome: Result<(bool,), _> = session.with_proxy("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", ASK).method_call("org.gnome.ScreenSaver", "GetActive", ());
500            answers.gnome_active = gnome.ok().map(|(on,)| on);
501        }
502        if let Some(system) = system {
503            let login = system.with_proxy("org.freedesktop.login1", "/org/freedesktop/login1/session/auto", ASK);
504            answers.locked_hint = login.get::<bool>("org.freedesktop.login1.Session", "LockedHint").ok();
505            if login.get::<bool>("org.freedesktop.login1.Session", "IdleHint").unwrap_or(false) {
506                answers.idle_since_us = login.get::<u64>("org.freedesktop.login1.Session", "IdleSinceHint").ok();
507            }
508        }
509        // logind's time is the real clock's: with the clock before 1970, not read.
510        let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
511            Ok(now) => now,
512            Err(_) => {
513                answers.idle_since_us = None;
514                Duration::ZERO
515            }
516        };
517        linux_session(&answers, now)
518    }
519}
520
521#[cfg(windows)]
522mod imp {
523    use super::{Power, Session, idle_of, windows_session};
524
525    /// `SYSTEM_POWER_STATUS`.
526    #[repr(C)]
527    #[derive(Default)]
528    struct PowerStatus {
529        ac_line_status: u8,
530        battery_flag: u8,
531        battery_life_percent: u8,
532        /// 1 while the battery saver is on.
533        system_status_flag: u8,
534        battery_life_time: u32,
535        battery_full_life_time: u32,
536    }
537
538    /// `LASTINPUTINFO`.
539    #[repr(C)]
540    struct LastInput {
541        size: u32,
542        time: u32,
543    }
544
545    #[link(name = "kernel32")]
546    unsafe extern "system" {
547        fn GetSystemPowerStatus(status: *mut PowerStatus) -> i32;
548        fn GetTickCount() -> u32;
549    }
550
551    #[link(name = "user32")]
552    unsafe extern "system" {
553        fn GetLastInputInfo(info: *mut LastInput) -> i32;
554    }
555
556    #[link(name = "wtsapi32")]
557    unsafe extern "system" {
558        fn WTSQuerySessionInformationW(server: *mut std::ffi::c_void, session: u32, class: i32, buffer: *mut *mut u16, bytes: *mut u32) -> i32;
559        fn WTSFreeMemory(memory: *mut std::ffi::c_void);
560    }
561
562    /// `WTS_CURRENT_SESSION`: the session this program runs in.
563    const CURRENT_SESSION: u32 = u32::MAX;
564    /// `WTSSessionInfoEx`, among the classes of `WTS_INFO_CLASS`.
565    const SESSION_INFO_EX: i32 = 25;
566
567    /// Something is read here.
568    pub(super) const READS: bool = true;
569
570    /// Nothing kept between readings: Windows answers each question at once.
571    pub(super) struct Reader;
572
573    impl Reader {
574        pub(super) fn here() -> Reader {
575            Reader
576        }
577
578        /// No D-Bus here: the addresses are not used.
579        pub(super) fn at(_session: Option<String>, _system: Option<String>) -> Reader {
580            Reader
581        }
582
583        /// Everything, read now: the power supply, then the session.
584        pub(super) fn read(&mut self) -> (Session, Power) {
585            let session = session();
586            let power = Power { idle: idle_of(session.locked, session.since_input), ..supply() };
587            (session, power)
588        }
589    }
590
591    /// The AC line and the battery saver (`GetSystemPowerStatus`).
592    fn supply() -> Power {
593        let mut status = PowerStatus::default();
594        // SAFETY: it fills the structure it is given, laid out as Windows declares it.
595        let read = unsafe { GetSystemPowerStatus(&mut status) } != 0;
596        let on_mains = if read {
597            match status.ac_line_status {
598                0 => Some(false),
599                1 => Some(true),
600                _ => None,
601            }
602        } else {
603            None
604        };
605        let power_saver = read.then_some(status.system_status_flag == 1);
606        Power { on_mains, power_saver, ..Power::default() }
607    }
608
609    /// The tick counts now and at the last input, and `WTSSessionInfoEx`'s
610    /// answer, read as `windows_session` says.
611    fn session() -> Session {
612        let mut input = LastInput { size: std::mem::size_of::<LastInput>() as u32, time: 0 };
613        // SAFETY: it fills the structure, its size set.
614        let given = unsafe { GetLastInputInfo(&mut input) } != 0;
615        // SAFETY: it reads a counter.
616        let ticks = given.then(|| (unsafe { GetTickCount() }, input.time));
617        let mut buffer: *mut u16 = std::ptr::null_mut();
618        let mut bytes: u32 = 0;
619        // SAFETY: the current server (null) and session; Windows allocates the answer and says its size.
620        let asked = unsafe { WTSQuerySessionInformationW(std::ptr::null_mut(), CURRENT_SESSION, SESSION_INFO_EX, &mut buffer, &mut bytes) } != 0;
621        if !asked || buffer.is_null() {
622            return windows_session(ticks, None);
623        }
624        // SAFETY: Windows gave `bytes` bytes at `buffer`: read as bytes, within
625        // them, before it is given back, once.
626        unsafe {
627            let session = windows_session(ticks, Some(std::slice::from_raw_parts(buffer.cast::<u8>(), bytes as usize)));
628            WTSFreeMemory(buffer.cast());
629            session
630        }
631    }
632}
633
634#[cfg(target_os = "macos")]
635mod imp {
636    use super::{Power, Session, idle_of, macos_session};
637    use std::ffi::{CStr, c_char, c_void};
638    use std::time::{Duration, Instant};
639
640    #[link(name = "IOKit", kind = "framework")]
641    unsafe extern "C" {
642        fn IOPSCopyPowerSourcesInfo() -> *const c_void;
643        fn IOPSGetProvidingPowerSourceType(snapshot: *const c_void) -> *const c_void;
644    }
645
646    #[link(name = "CoreFoundation", kind = "framework")]
647    unsafe extern "C" {
648        fn CFStringGetCString(string: *const c_void, buffer: *mut c_char, size: isize, encoding: u32) -> u8;
649        fn CFRelease(object: *const c_void);
650        fn CFStringCreateWithCString(allocator: *const c_void, text: *const c_char, encoding: u32) -> *const c_void;
651        fn CFDictionaryGetValue(dictionary: *const c_void, key: *const c_void) -> *const c_void;
652        fn CFGetTypeID(object: *const c_void) -> usize;
653        fn CFBooleanGetTypeID() -> usize;
654        fn CFBooleanGetValue(boolean: *const c_void) -> u8;
655    }
656
657    #[link(name = "CoreGraphics", kind = "framework")]
658    unsafe extern "C" {
659        fn CGEventSourceSecondsSinceLastEventType(state: i32, event_type: u32) -> f64;
660        fn CGSessionCopyCurrentDictionary() -> *const c_void;
661    }
662
663    /// `kCFStringEncodingUTF8`.
664    const UTF8: u32 = 0x0800_0100;
665
666    /// Something is read here.
667    pub(super) const READS: bool = true;
668
669    /// How often Low Power Mode is asked (`pmset`, a program started each time).
670    const LOW_POWER_EVERY: Duration = Duration::from_secs(5 * 60);
671
672    /// What the reading thread keeps between its readings: Low Power Mode as
673    /// `pmset` last said it, and when.
674    pub(super) struct Reader {
675        low_power: Option<(Instant, Option<bool>)>,
676    }
677
678    impl Reader {
679        pub(super) fn here() -> Reader {
680            Reader { low_power: None }
681        }
682
683        /// No D-Bus here: the addresses are not used.
684        pub(super) fn at(_session: Option<String>, _system: Option<String>) -> Reader {
685            Reader::here()
686        }
687
688        /// Everything, read now: the power source, Low Power Mode (asked
689        /// again every `LOW_POWER_EVERY`), the session.
690        pub(super) fn read(&mut self) -> (Session, Power) {
691            let power_saver = match self.low_power {
692                Some((at, said)) if at.elapsed() < LOW_POWER_EVERY => said,
693                _ => {
694                    let said = low_power_mode();
695                    self.low_power = Some((Instant::now(), said));
696                    said
697                }
698            };
699            let session = session();
700            let power = Power { on_mains: on_mains(), power_saver, idle: idle_of(session.locked, session.since_input), metered: None };
701            (session, power)
702        }
703    }
704
705    /// The time since the session's last input, and whether its screen is
706    /// locked, read as `macos_session` says.
707    fn session() -> Session {
708        // The session's every input (`kCGEventSourceStateCombinedSessionState`, `kCGAnyInputEventType`).
709        // SAFETY: it only reads the session's state.
710        let seconds = unsafe { CGEventSourceSecondsSinceLastEventType(0, u32::MAX) };
711        macos_session(seconds, screen_locked())
712    }
713
714    /// Whether the screen is locked, as the window server says it of this
715    /// session (`CGSSessionScreenIsLocked`, there only while it is).
716    fn screen_locked() -> bool {
717        // SAFETY: the session's dictionary and the key are ours, each released
718        // once; the value is borrowed from the dictionary and read before it is.
719        unsafe {
720            let session = CGSessionCopyCurrentDictionary();
721            if session.is_null() {
722                return false;
723            }
724            let key = CFStringCreateWithCString(std::ptr::null(), c"CGSSessionScreenIsLocked".as_ptr(), UTF8);
725            let value = if key.is_null() { std::ptr::null() } else { CFDictionaryGetValue(session, key) };
726            let locked = !value.is_null() && CFGetTypeID(value) == CFBooleanGetTypeID() && CFBooleanGetValue(value) != 0;
727            if !key.is_null() {
728                CFRelease(key);
729            }
730            CFRelease(session);
731            locked
732        }
733    }
734
735    /// What provides the power now, as IOKit names it: "AC Power", "Battery Power", "UPS Power".
736    fn on_mains() -> Option<bool> {
737        // SAFETY: the snapshot is ours, released once; the type it names is
738        // borrowed from it and read before that, into a buffer of ours.
739        unsafe {
740            let snapshot = IOPSCopyPowerSourcesInfo();
741            if snapshot.is_null() {
742                return None;
743            }
744            let kind = IOPSGetProvidingPowerSourceType(snapshot);
745            let mut buffer = [0 as c_char; 64];
746            let read = !kind.is_null() && CFStringGetCString(kind, buffer.as_mut_ptr(), buffer.len() as isize, UTF8) != 0;
747            let name = read.then(|| CStr::from_ptr(buffer.as_ptr()).to_string_lossy().into_owned());
748            CFRelease(snapshot);
749            name.map(|n| n == "AC Power")
750        }
751    }
752
753    /// Low Power Mode, as `pmset -g` says it ("lowpowermode 1"); a Mac without it says nothing of it: off.
754    fn low_power_mode() -> Option<bool> {
755        let out = std::process::Command::new("pmset").arg("-g").output().ok().filter(|o| o.status.success())?;
756        let text = String::from_utf8_lossy(&out.stdout);
757        Some(text.lines().any(|line| {
758            let mut words = line.split_whitespace();
759            words.next() == Some("lowpowermode") && words.next() == Some("1")
760        }))
761    }
762}
763
764#[cfg(any(target_os = "android", not(any(unix, windows))))]
765mod imp {
766    /// Nothing is read here: no reading thread (`start`), `read` says nothing at once.
767    pub(super) const READS: bool = false;
768
769    pub(super) struct Reader;
770
771    impl Reader {
772        pub(super) fn here() -> Reader {
773            Reader
774        }
775
776        pub(super) fn at(_session: Option<String>, _system: Option<String>) -> Reader {
777            Reader
778        }
779
780        pub(super) fn read(&mut self) -> (super::Session, super::Power) {
781            Default::default()
782        }
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    /// NetworkManager's five answers: a guess counts as what it guesses.
791    #[test]
792    fn metered_as_networkmanager_says() {
793        assert_eq!([0, 1, 2, 3, 4, 9].map(metered_of), [None, Some(true), Some(false), Some(true), Some(false), None]);
794    }
795
796    /// Locked is idle; else the newest touch says it; nothing heard, unknown.
797    #[test]
798    fn idle_as_the_session_says() {
799        let minutes = |m: u64| Some(Duration::from_secs(m * 60));
800        assert_eq!(idle_of(true, None), Some(true));
801        assert_eq!(idle_of(true, minutes(1)), Some(true));
802        assert_eq!(idle_of(false, minutes(15)), Some(true));
803        assert_eq!(idle_of(false, minutes(14)), Some(false));
804        assert_eq!(idle_of(false, None), None);
805    }
806
807    /// Long work needs mains power and no power saving, both known.
808    #[test]
809    fn supplied_needs_both_known() {
810        let p = |on_mains, power_saver| Power { on_mains, power_saver, ..Power::default() };
811        assert!(p(Some(true), Some(false)).supplied());
812        for (on_mains, saver) in [(Some(false), Some(false)), (Some(true), Some(true)), (None, Some(false)), (Some(true), None), (None, None)] {
813            assert!(!p(on_mains, saver).supplied(), "{on_mains:?} {saver:?}");
814        }
815    }
816
817    fn minutes(m: u64) -> Duration {
818        Duration::from_secs(m * 60)
819    }
820
821    /// A person is at a computer while its session is unlocked and was
822    /// touched within ten minutes; a press in Sioul counts at once, whatever
823    /// the system says of input; locked, nobody is. A system that says nothing
824    /// of input says nothing: Sioul running counts, as before.
825    #[test]
826    fn a_person_is_at_a_computer_as_its_session_says() {
827        let session = |since: Option<u64>, locked: bool| Session { since_input: since.map(minutes), locked, disputed: false };
828        assert_eq!(present_of(session(Some(0), false), None), Some(true));
829        assert_eq!(present_of(session(Some(9), false), None), Some(true));
830        assert_eq!(present_of(session(Some(10), false), None), Some(false), "ten minutes without input: nobody");
831        assert_eq!(present_of(session(Some(120), false), None), Some(false));
832        // A press in Sioul's window or of do-not-disturb's switch: at once, the system's idle time aside.
833        assert_eq!(present_of(session(Some(120), false), Some(Duration::from_secs(5))), Some(true));
834        assert_eq!(present_of(session(None, false), Some(minutes(9))), Some(true));
835        assert_eq!(present_of(session(Some(120), false), Some(minutes(10))), Some(false), "a press ten minutes ago no longer counts");
836        // Locked: nobody, a press or input a moment ago included.
837        assert_eq!(present_of(session(Some(0), true), Some(Duration::ZERO)), Some(false));
838        assert_eq!(present_of(session(None, true), None), Some(false));
839        // Nothing said of input, no press: unknown (Sioul running counts, as before).
840        assert_eq!(present_of(session(None, false), None), None);
841        assert_eq!(present_of(session(None, false), Some(minutes(11))), None);
842    }
843
844    /// Linux and the BSDs, from stand-in answers of the session's services:
845    /// a locker locks unless another says unlocked; the newest touch any of
846    /// them gave wins; logind's idle time is the real clock less its hint;
847    /// none answering, nothing said.
848    #[test]
849    fn a_linux_session_read_from_its_services() {
850        let now = Duration::from_secs(1_800_000_000);
851        let none = LinuxAnswers::default();
852        assert_eq!(linux_session(&none, now), Session::default(), "no service answered: nothing said");
853        // KDE: its screen saver alone.
854        let kde = LinuxAnswers { saver_active: Some(false), saver_idle_ms: Some(30_000), ..none };
855        assert_eq!(linux_session(&kde, now), Session { since_input: Some(Duration::from_secs(30)), locked: false, disputed: false });
856        assert_eq!(present_of(linux_session(&kde, now), None), Some(true));
857        assert_eq!(linux_session(&LinuxAnswers { saver_active: Some(true), ..kde }, now).locked, true, "KDE's locker on");
858        // GNOME: Mutter's idle monitor and its shield; the freedesktop name not there.
859        let gnome = LinuxAnswers { mutter_idle_ms: Some(11 * 60_000), gnome_active: Some(false), locked_hint: Some(false), ..none };
860        assert_eq!(present_of(linux_session(&gnome, now), None), Some(false), "eleven minutes without input");
861        assert!(linux_session(&LinuxAnswers { gnome_active: Some(true), locked_hint: Some(true), ..gnome }, now).locked, "GNOME locked: its shield and logind say so");
862        assert!(linux_session(&LinuxAnswers { gnome_active: Some(true), ..gnome }, now).disputed, "its shield up, logind saying unlocked: disputed");
863        // Several monitors: the newest touch wins.
864        let both = LinuxAnswers { saver_idle_ms: Some(20 * 60_000), mutter_idle_ms: Some(60_000), ..none };
865        assert_eq!(linux_session(&both, now).since_input, Some(minutes(1)));
866        // logind alone (a desktop with neither): its hints; idle since a time of the real clock.
867        let logind = LinuxAnswers { locked_hint: Some(false), idle_since_us: Some((now - minutes(25)).as_micros() as u64), ..none };
868        assert_eq!(linux_session(&logind, now).since_input, Some(minutes(25)));
869        assert!(linux_session(&LinuxAnswers { locked_hint: Some(true), ..none }, now).locked);
870        assert_eq!(present_of(linux_session(&LinuxAnswers { locked_hint: Some(false), ..none }, now), None), None, "logind not idle, no monitor: unknown");
871    }
872
873    /// Windows, from stand-in answers: the tick counts (wrapping), and
874    /// `WTSSessionInfoEx`'s answer laid out as Windows lays it.
875    #[test]
876    fn a_windows_session_read_from_its_answers() {
877        let wts = |level: u32, flags: i32, length: usize| -> Vec<u8> {
878            let mut answer = vec![0u8; 360];
879            answer[0..4].copy_from_slice(&level.to_le_bytes());
880            answer[8..12].copy_from_slice(&1u32.to_le_bytes());
881            answer[12..16].copy_from_slice(&0u32.to_le_bytes());
882            answer[16..20].copy_from_slice(&flags.to_le_bytes());
883            answer.truncate(length);
884            answer
885        };
886        let locked = wts(1, 0, 360);
887        let unlocked = wts(1, 1, 360);
888        assert_eq!(windows_flags(&locked), Some(0));
889        assert_eq!(windows_session(Some((70_000, 10_000)), Some(&unlocked)), Session { since_input: Some(minutes(1)), locked: false, disputed: false });
890        assert!(windows_session(Some((70_000, 10_000)), Some(&locked)).locked);
891        assert_eq!(present_of(windows_session(Some((70_000, 10_000)), Some(&locked)), None), Some(false));
892        // Unknown, another level, an answer too short, none at all: not said locked.
893        for answer in [wts(1, -1, 360), wts(2, 0, 360), wts(1, 0, 18)] {
894            assert!(!windows_session(None, Some(&answer)).locked, "{answer:?}");
895        }
896        assert_eq!(windows_session(None, None), Session::default(), "nothing answered: nothing said");
897        // The tick count wrapped since the last input (49.7 days): still the time between.
898        assert_eq!(windows_session(Some((5_000, u32::MAX - 54_999)), None).since_input, Some(minutes(1)));
899    }
900
901    /// macOS, from stand-in answers: the seconds since the last input (a value
902    /// that is no time says nothing), and the screen's lock.
903    #[test]
904    fn a_macos_session_read_from_its_answers() {
905        assert_eq!(macos_session(42.5, false), Session { since_input: Some(Duration::from_millis(42_500)), locked: false, disputed: false });
906        assert_eq!(present_of(macos_session(42.5, false), None), Some(true));
907        assert_eq!(present_of(macos_session(42.5, true), None), Some(false));
908        assert_eq!(present_of(macos_session(900.0, false), None), Some(false));
909        for odd in [f64::NAN, f64::INFINITY, -1.0, f64::MAX] {
910            assert_eq!(macos_session(odd, false).since_input, None, "{odd}");
911        }
912    }
913
914    /// Lock answers that disagree are not taken; input alone says whether
915    /// someone is there (review of battery part C, F4 and G3): logind's hint
916    /// left locked by a locker that died, the desktop's own screen saver
917    /// saying unlocked and input five seconds ago: someone; GNOME's shield up
918    /// without a lock behind it (its automatic lock off), two hours without
919    /// input: nobody; no input said: unknown, Sioul running counts. A press in
920    /// Sioul says someone.
921    #[test]
922    fn lock_answers_that_disagree_leave_input_to_say_it() {
923        let now = Duration::from_secs(1_800_000_000);
924        let stale = LinuxAnswers { saver_active: Some(false), saver_idle_ms: Some(5_000), locked_hint: Some(true), ..LinuxAnswers::default() };
925        let session = linux_session(&stale, now);
926        assert!(session.disputed && !session.locked);
927        assert_eq!(present_of(session, None), Some(true), "input five seconds ago: someone, the stale lock aside");
928        let shield = LinuxAnswers { mutter_idle_ms: Some(2 * 3_600_000), gnome_active: Some(true), locked_hint: Some(false), ..LinuxAnswers::default() };
929        let night = linux_session(&shield, now);
930        assert!(night.disputed);
931        assert_eq!(present_of(night, None), Some(false), "GNOME's shield up, no lock, idle two hours: nobody");
932        assert_eq!(present_of(Session { since_input: None, ..night }, None), None, "nothing said of input: unknown, Sioul running counts");
933        assert_eq!(present_of(night, Some(Duration::from_secs(3))), Some(true), "a press says someone");
934        // Agreeing lockers: locked; agreeing unlocked: by input, as before.
935        let both_locked = LinuxAnswers { saver_active: Some(true), locked_hint: Some(true), ..LinuxAnswers::default() };
936        assert_eq!(present_of(linux_session(&both_locked, now), None), Some(false));
937        let both_unlocked = LinuxAnswers { saver_active: Some(false), saver_idle_ms: Some(5_000), locked_hint: Some(false), ..LinuxAnswers::default() };
938        assert_eq!(present_of(linux_session(&both_unlocked, now), None), Some(true));
939        // The spam training reads a disputed lock as unlocked, by input alone.
940        assert_eq!(idle_of(session.locked, Some(minutes(20))), Some(true));
941    }
942
943    /// A press in Sioul made after the session was last read wins over what
944    /// that reading said, its lock included: someone unlocked the computer
945    /// to make it (review of battery part C, F4). A press older than the
946    /// reading counts as `present_of` says; no reading, a recent press says
947    /// someone and nothing else says anything.
948    #[test]
949    fn a_press_newer_than_the_reading_wins_over_its_lock() {
950        let locked = Session { since_input: Some(minutes(40)), locked: true, disputed: false };
951        let secs = Duration::from_secs;
952        assert_eq!(present_since(Some((locked, secs(20))), Some(secs(2))), Some(true), "read locked 20 s ago, pressed 2 s ago");
953        assert_eq!(present_since(Some((locked, secs(20))), Some(secs(30))), Some(false), "pressed before the reading that says locked");
954        assert_eq!(present_since(Some((locked, secs(20))), None), Some(false));
955        let away = Session { since_input: Some(minutes(40)), locked: false, disputed: false };
956        assert_eq!(present_since(Some((away, secs(20))), Some(secs(30))), Some(true), "unlocked, pressed within ten minutes");
957        assert_eq!(present_since(Some((away, secs(20))), Some(minutes(11))), Some(false));
958        assert_eq!(present_since(None, Some(secs(30))), Some(true), "no reading: a press says someone");
959        assert_eq!(present_since(None, Some(minutes(11))), None, "no reading: unknown");
960        assert_eq!(present_since(None, None), None);
961    }
962
963    /// A reading is timed from when it began, not from when it came back: a
964    /// press made while a slow reading said locked (someone unlocked the
965    /// computer meanwhile, then pressed) is newer than it and wins (review of
966    /// battery part C, G6).
967    #[test]
968    fn a_press_made_during_a_slow_reading_wins_over_its_lock() {
969        let locked = Session { since_input: Some(minutes(40)), locked: true, disputed: false };
970        let began = Instant::now();
971        let mut pressed = None;
972        let reading = reading_of(began, || {
973            std::thread::sleep(Duration::from_millis(150));
974            pressed = Some(Instant::now());
975            std::thread::sleep(Duration::from_millis(150));
976            (locked, Power::default())
977        });
978        assert_eq!(reading.began, began, "stamped when it began");
979        let now = Instant::now();
980        let (age, ago) = (now.duration_since(reading.began), now.duration_since(pressed.unwrap()));
981        assert!(ago < age);
982        assert_eq!(present_since(Some((reading.session, age)), Some(ago)), Some(true));
983    }
984
985    /// The kept reading: fresh, its own; once `KEPT_FOR` old, none (a bus
986    /// that stopped answering, the thread resting), and Sioul running counts.
987    #[test]
988    fn the_kept_reading_ages_out() {
989        let session = Session { since_input: Some(Duration::from_secs(4)), locked: false, disputed: false };
990        let reading = Reading { began: Instant::now(), session, power: Power { metered: Some(true), ..Power::default() } };
991        assert_eq!(fresh(Some(reading), reading.began + Duration::from_secs(1)).map(|r| (r.session, r.power.metered)), Some((session, Some(true))));
992        assert!(fresh(Some(reading), reading.began + KEPT_FOR - Duration::from_millis(1)).is_some());
993        assert!(fresh(Some(reading), reading.began + KEPT_FOR).is_none(), "too old: none");
994        assert!(fresh(None, reading.began).is_none());
995    }
996
997    /// The thread reads while it is asked for, and rests `ASKED_FOR` after
998    /// the last ask, until the next (a process that asked once is not read
999    /// every half minute for days).
1000    #[test]
1001    fn the_thread_reads_while_asked_then_rests() {
1002        let asked = Instant::now();
1003        assert!(!wanted(None, asked), "never asked: rests");
1004        assert!(wanted(Some(asked), asked + Duration::from_secs(60)));
1005        assert!(wanted(Some(asked), asked + ASKED_FOR - Duration::from_millis(1)));
1006        assert!(!wanted(Some(asked), asked + ASKED_FOR), "asked fifteen minutes ago: rests");
1007        // An ask rings whoever waits, and says when.
1008        static SHARED_HERE: Mutex<Shared> = Mutex::new(Shared { reading: None, asked: None });
1009        static BELL_HERE: Condvar = Condvar::new();
1010        ask(&SHARED_HERE, &BELL_HERE, asked);
1011        assert_eq!(SHARED_HERE.lock().unwrap().asked, Some(asked));
1012    }
1013
1014    /// `read` waits for a fresh reading `WAIT` at most: none, it comes back
1015    /// empty-handed at its limit; one rung meanwhile, at once.
1016    #[test]
1017    fn a_reading_is_waited_for_its_limit_at_most() {
1018        static SHARED_HERE: Mutex<Shared> = Mutex::new(Shared { reading: None, asked: None });
1019        static BELL_HERE: Condvar = Condvar::new();
1020        let start = Instant::now();
1021        assert!(fresh_within(&SHARED_HERE, &BELL_HERE, Duration::from_millis(200)).is_none());
1022        let took = start.elapsed();
1023        assert!(took >= Duration::from_millis(200) && took < Duration::from_secs(2), "{took:?}");
1024        let keeper = std::thread::spawn(|| {
1025            std::thread::sleep(Duration::from_millis(100));
1026            let session = Session { since_input: Some(Duration::from_secs(1)), locked: false, disputed: false };
1027            SHARED_HERE.lock().unwrap().reading = Some(Reading { began: Instant::now(), session, power: Power::default() });
1028            BELL_HERE.notify_all();
1029        });
1030        let start = Instant::now();
1031        let came = fresh_within(&SHARED_HERE, &BELL_HERE, Duration::from_secs(10));
1032        assert!(came.is_some_and(|reading| reading.session.since_input == Some(Duration::from_secs(1))));
1033        assert!(start.elapsed() < Duration::from_secs(5), "{:?}", start.elapsed());
1034        keeper.join().unwrap();
1035    }
1036
1037    /// Whether a reading was ever kept: none, waited for its limit and said
1038    /// no; one rung meanwhile, yes at once; afterwards, yes without waiting,
1039    /// however old it is (the window's first exchange: review of battery part
1040    /// C, L3).
1041    #[test]
1042    fn the_first_reading_is_waited_for_once() {
1043        static SHARED_HERE: Mutex<Shared> = Mutex::new(Shared { reading: None, asked: None });
1044        static BELL_HERE: Condvar = Condvar::new();
1045        let start = Instant::now();
1046        assert!(!ever_read_within(&SHARED_HERE, &BELL_HERE, Duration::from_millis(150)));
1047        assert!(start.elapsed() >= Duration::from_millis(150));
1048        let keeper = std::thread::spawn(|| {
1049            std::thread::sleep(Duration::from_millis(100));
1050            SHARED_HERE.lock().unwrap().reading = Some(Reading { began: Instant::now(), session: Session::default(), power: Power { metered: Some(true), ..Power::default() } });
1051            BELL_HERE.notify_all();
1052        });
1053        let start = Instant::now();
1054        assert!(ever_read_within(&SHARED_HERE, &BELL_HERE, Duration::from_secs(10)));
1055        assert!(start.elapsed() < Duration::from_secs(5), "{:?}", start.elapsed());
1056        keeper.join().unwrap();
1057        // Read once: at once from then on, the reading aged or not.
1058        SHARED_HERE.lock().unwrap().reading = Some(Reading { began: Instant::now() - KEPT_FOR.min(Duration::from_secs(1)), session: Session::default(), power: Power::default() });
1059        let start = Instant::now();
1060        assert!(ever_read_within(&SHARED_HERE, &BELL_HERE, Duration::from_secs(10)));
1061        assert!(start.elapsed() < Duration::from_millis(100));
1062    }
1063
1064    /// Nothing but the reading thread asks the system for these facts: the
1065    /// connections are made and the questions asked by `imp::Reader::read`
1066    /// alone, called on that thread only (`start`); what the others call
1067    /// reads what it kept (review of battery part C, G1).
1068    #[test]
1069    fn nothing_but_the_reading_thread_asks_the_system() {
1070        let source = include_str!("power.rs");
1071        let shared = source.split("#[cfg(all(unix, not(any(target_os = \"macos\", target_os = \"android\"))))]\nmod imp {").next().unwrap();
1072        assert_eq!(shared.matches("reader.read()").count(), 1, "read once, on the thread");
1073        let thread = shared.split("fn start(").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
1074        assert!(thread.contains("spawn(move || {\n            let mut reader = reader();") && thread.contains("reader.read()"), "the reader made and read on the thread it starts");
1075        for public in ["pub fn read() -> Power {", "pub fn supply() -> Power {", "pub fn kept_session()", "pub fn kept_power()"] {
1076            let body = shared.split(public).nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
1077            assert!(!body.contains("Reader") && !body.contains("Connection"), "{public} reads what is kept");
1078        }
1079        let linux = source.split("mod imp {").nth(1).and_then(|rest| rest.split("#[cfg(windows)]").next()).unwrap();
1080        assert!(linux.contains("crate::bus::connect(") && !linux.contains("Channel::"), "its connections are crate::bus's (bus::tests::nothing_but_this_file_reaches_a_bus)");
1081    }
1082
1083    /// Two stand-in buses (`bus::StandIns`: private `dbus-daemon`s, never the
1084    /// session or the system of whoever runs this) as the reading thread
1085    /// meets them (review of battery part C, G1):
1086    /// - both healthy: three readings, one connection to each bus, kept;
1087    /// - the session's stopped (SIGSTOP) once connected: a reading comes back,
1088    ///   its questions given up a second each, and keeps its connection;
1089    /// - the session's stand-in ends and another takes its place: the next
1090    ///   reading connects again.
1091    ///
1092    /// A bus stuck at setup holding nothing else is `bus`'s own test
1093    /// (`bus::tests::a_bus_stuck_at_setup_holds_nothing_else`). Starts
1094    /// `dbus-daemon`: run alone, by name:
1095    /// `cargo test --release -p sioul-sync power::tests::the_reading_keeps_its_connections_through_a_stopped_bus -- --ignored --exact`.
1096    #[test]
1097    #[ignore = "starts two stand-in dbus-daemons: run alone, by name"]
1098    #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
1099    fn the_reading_keeps_its_connections_through_a_stopped_bus() {
1100        use crate::bus::StandIns;
1101        use dbus::blocking::Connection;
1102        // The unique names on a bus: one per connection, never given twice.
1103        let names = |look: &Connection| -> Vec<String> {
1104            let (all,): (Vec<String>,) = look.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(2)).method_call("org.freedesktop.DBus", "ListNames", ()).unwrap();
1105            let mut unique: Vec<String> = all.into_iter().filter(|name| name.starts_with(':')).collect();
1106            unique.sort();
1107            unique
1108        };
1109        let look = |address: &str| Connection::from(crate::bus::connect(address).unwrap());
1110        let mut stand_ins = StandIns::new("stand-in-buses");
1111        let (session_pid, session) = stand_ins.start("session");
1112        let (_, system) = stand_ins.start("system");
1113
1114        // Both healthy: one connection to each, kept.
1115        let (look_session, look_system) = (look(&session), look(&system));
1116        let mut reader = imp::Reader::at(Some(session.clone()), Some(system.clone()));
1117        for _ in 0..3 {
1118            let start = Instant::now();
1119            let _ = reader.read();
1120            assert!(start.elapsed() < Duration::from_secs(2), "{:?}", start.elapsed());
1121        }
1122        let before = names(&look_session);
1123        assert_eq!(before.len(), 2, "the test's and the reader's: {before:?}");
1124        assert_eq!(names(&look_system).len(), 2);
1125
1126        // The session's bus stops answering: a reading comes back, its questions given up.
1127        StandIns::signal(session_pid, "-STOP");
1128        let start = Instant::now();
1129        let _ = reader.read();
1130        let took = start.elapsed();
1131        StandIns::signal(session_pid, "-CONT");
1132        assert!(took < Duration::from_secs(8), "its questions given up a second each: {took:?}");
1133        // The reader kept its connection through the questions given up.
1134        let _ = reader.read();
1135        assert_eq!(names(&look_session), before, "the same connection kept");
1136
1137        // The session's stand-in ends, another takes its place: connected again.
1138        StandIns::signal(session_pid, "-KILL");
1139        std::thread::sleep(Duration::from_millis(200));
1140        let (_, again) = stand_ins.start("session");
1141        assert_eq!(again, session);
1142        let look_again = look(&session);
1143        let _ = reader.read();
1144        assert_eq!(names(&look_again).len(), 2, "the test's and the reader's, made again");
1145    }
1146}