1use std::sync::{Condvar, Mutex, Once};
46use std::time::{Duration, Instant};
47
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50pub struct Power {
51 pub on_mains: Option<bool>,
53 pub power_saver: Option<bool>,
55 pub idle: Option<bool>,
57 pub metered: Option<bool>,
59}
60
61impl Power {
62 pub fn supplied(&self) -> bool {
65 self.on_mains == Some(true) && self.power_saver == Some(false)
66 }
67}
68
69pub const IDLE: Duration = Duration::from_secs(15 * 60);
71
72#[cfg_attr(not(all(unix, not(any(target_os = "macos", target_os = "android")))), allow(dead_code))]
74const ASK: Duration = Duration::from_secs(1);
75
76pub 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
90pub fn supply() -> Power {
93 let power = read();
94 Power { on_mains: power.on_mains, power_saver: power.power_saver, ..Power::default() }
95}
96
97pub 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
107pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub struct Session {
119 pub since_input: Option<Duration>,
122 pub locked: bool,
125 pub disputed: bool,
130}
131
132pub const PRESENT: Duration = Duration::from_secs(10 * 60);
134
135pub 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
152pub 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
168pub const EVERY: Duration = Duration::from_secs(30);
170pub const WAIT: Duration = Duration::from_secs(10);
173pub const KEPT_FOR: Duration = Duration::from_secs(120);
177pub const ASKED_FOR: Duration = Duration::from_secs(15 * 60);
183
184#[derive(Debug, Clone, Copy)]
188struct Reading {
189 began: Instant,
190 session: Session,
191 power: Power,
192}
193
194struct Shared {
197 reading: Option<Reading>,
198 asked: Option<Instant>,
199}
200
201static SHARED: Mutex<Shared> = Mutex::new(Shared { reading: None, asked: None });
202static BELL: Condvar = Condvar::new();
204static STARTED: Once = Once::new();
206
207fn reading_of(began: Instant, read: impl FnOnce() -> (Session, Power)) -> Reading {
211 let (session, power) = read();
212 Reading { began, session, power }
213}
214
215pub fn watch() {
221 start(imp::Reader::here);
222 ask(&SHARED, &BELL, Instant::now());
223}
224
225pub 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
234fn 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
261fn 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
269fn wanted(asked: Option<Instant>, now: Instant) -> bool {
271 asked.is_some_and(|at| now.saturating_duration_since(at) < ASKED_FOR)
272}
273
274fn fresh(reading: Option<Reading>, now: Instant) -> Option<Reading> {
276 reading.filter(|reading| now.saturating_duration_since(reading.began) < KEPT_FOR)
277}
278
279fn 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
287pub 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
296pub fn kept_power() -> Option<Power> {
300 fresh(SHARED.lock().ok()?.reading, Instant::now()).map(|reading| reading.power)
301}
302
303pub fn read_once_within(limit: Duration) -> bool {
307 ever_read_within(&SHARED, &BELL, limit)
308}
309
310fn 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#[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 saver_active: Option<bool>,
325 saver_idle_ms: Option<u32>,
328 mutter_idle_ms: Option<u64>,
330 gnome_active: Option<bool>,
332 locked_hint: Option<bool>,
334 idle_since_us: Option<u64>,
337}
338
339#[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#[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#[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#[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#[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 pub(super) const READS: bool = true;
403
404 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 pub(super) fn here() -> Reader {
418 Reader::at(crate::bus::session_address(), Some(crate::bus::system_address()))
419 }
420
421 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 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 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 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 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 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 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 #[repr(C)]
527 #[derive(Default)]
528 struct PowerStatus {
529 ac_line_status: u8,
530 battery_flag: u8,
531 battery_life_percent: u8,
532 system_status_flag: u8,
534 battery_life_time: u32,
535 battery_full_life_time: u32,
536 }
537
538 #[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 const CURRENT_SESSION: u32 = u32::MAX;
564 const SESSION_INFO_EX: i32 = 25;
566
567 pub(super) const READS: bool = true;
569
570 pub(super) struct Reader;
572
573 impl Reader {
574 pub(super) fn here() -> Reader {
575 Reader
576 }
577
578 pub(super) fn at(_session: Option<String>, _system: Option<String>) -> Reader {
580 Reader
581 }
582
583 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 fn supply() -> Power {
593 let mut status = PowerStatus::default();
594 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 fn session() -> Session {
612 let mut input = LastInput { size: std::mem::size_of::<LastInput>() as u32, time: 0 };
613 let given = unsafe { GetLastInputInfo(&mut input) } != 0;
615 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 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 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 const UTF8: u32 = 0x0800_0100;
665
666 pub(super) const READS: bool = true;
668
669 const LOW_POWER_EVERY: Duration = Duration::from_secs(5 * 60);
671
672 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 pub(super) fn at(_session: Option<String>, _system: Option<String>) -> Reader {
685 Reader::here()
686 }
687
688 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 fn session() -> Session {
708 let seconds = unsafe { CGEventSourceSecondsSinceLastEventType(0, u32::MAX) };
711 macos_session(seconds, screen_locked())
712 }
713
714 fn screen_locked() -> bool {
717 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 fn on_mains() -> Option<bool> {
737 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 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 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 #[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 #[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 #[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 #[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 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 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 assert_eq!(present_of(session(None, false), None), None);
841 assert_eq!(present_of(session(None, false), Some(minutes(11))), None);
842 }
843
844 #[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 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 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 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 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 #[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 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 assert_eq!(windows_session(Some((5_000, u32::MAX - 54_999)), None).since_input, Some(minutes(1)));
899 }
900
901 #[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 #[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 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 assert_eq!(idle_of(session.locked, Some(minutes(20))), Some(true));
941 }
942
943 #[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 #[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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 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 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 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 let _ = reader.read();
1135 assert_eq!(names(&look_session), before, "the same connection kept");
1136
1137 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}