use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use kanade_shared::manifest::{EnvState, Require, require_met};
static LATEST_SYSTEM_CPU: AtomicU32 = AtomicU32::new(f32::NAN.to_bits());
pub fn set_system_cpu(pct: Option<f32>) {
let bits = pct.unwrap_or(f32::NAN).to_bits();
LATEST_SYSTEM_CPU.store(bits, Ordering::Relaxed);
}
fn system_cpu() -> Option<f64> {
let v = f32::from_bits(LATEST_SYSTEM_CPU.load(Ordering::Relaxed));
if v.is_nan() { None } else { Some(v as f64) }
}
static LATEST_CONSOLE_IDLE: Mutex<Option<(Instant, Duration)>> = Mutex::new(None);
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) const SESSION_IDLE_SAMPLE_INTERVAL: Duration = Duration::from_secs(10);
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
const SESSION_IDLE_STALE_AFTER: Duration = Duration::from_secs(35);
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn set_console_idle(idle: Option<Duration>) {
let mut g = LATEST_CONSOLE_IDLE
.lock()
.unwrap_or_else(|e| e.into_inner());
*g = idle.map(|d| (Instant::now(), d));
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequireStatus {
Satisfied,
NotSatisfied,
Unsupported(Vec<&'static str>),
}
pub fn require_status(req: &Require) -> RequireStatus {
if req.is_empty() {
return RequireStatus::Satisfied; }
let unsupported = unsupported_gates(req);
if !unsupported.is_empty() {
return RequireStatus::Unsupported(unsupported);
}
#[cfg(target_os = "windows")]
let env = {
let (ac_online, idle, network_up) = sense_windows();
EnvState {
ac_online,
idle,
cpu_pct: system_cpu(),
network_up,
}
};
#[cfg(not(target_os = "windows"))]
let env = EnvState {
cpu_pct: system_cpu(),
..EnvState::default()
};
if require_met(req, &env) {
RequireStatus::Satisfied
} else {
RequireStatus::NotSatisfied
}
}
pub fn unsupported_gates(req: &Require) -> Vec<&'static str> {
if cfg!(target_os = "windows") {
return Vec::new();
}
let mut gates = Vec::new();
if req.ac_power {
gates.push("ac_power");
}
if req.idle.is_some() {
gates.push("idle");
}
if req.network {
gates.push("network");
}
gates
}
#[cfg(target_os = "windows")]
fn sense_windows() -> (bool, Option<std::time::Duration>, bool) {
use windows::Win32::NetworkManagement::IpHelper::GetNetworkConnectivityHint;
use windows::Win32::Networking::WinSock::{
NL_NETWORK_CONNECTIVITY_HINT, NetworkConnectivityLevelHintInternetAccess,
};
use windows::Win32::System::Power::{GetSystemPowerStatus, SYSTEM_POWER_STATUS};
let ac_online = {
let mut st = SYSTEM_POWER_STATUS::default();
match unsafe { GetSystemPowerStatus(&mut st) } {
Ok(()) => st.ACLineStatus == 1,
Err(_) => false,
}
};
let idle = console_idle();
let network_up = {
let mut hint = NL_NETWORK_CONNECTIVITY_HINT::default();
match unsafe { GetNetworkConnectivityHint(&mut hint) } {
ret if ret.0 == 0 => {
hint.ConnectivityLevel == NetworkConnectivityLevelHintInternetAccess
}
_ => false,
}
};
(ac_online, idle, network_up)
}
#[cfg(target_os = "windows")]
pub(crate) fn console_idle() -> Option<std::time::Duration> {
let g = LATEST_CONSOLE_IDLE
.lock()
.unwrap_or_else(|e| e.into_inner());
match *g {
Some((set_at, idle)) if set_at.elapsed() <= SESSION_IDLE_STALE_AFTER => Some(idle),
_ => Some(Duration::MAX),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_require_is_satisfied_without_syscalls() {
assert_eq!(
require_status(&Require::default()),
RequireStatus::Satisfied
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_reports_exactly_the_unsensed_gates() {
let all = Require {
ac_power: true,
idle: Some("10m".into()),
cpu_below: Some(20.0),
network: true,
};
assert_eq!(
require_status(&all),
RequireStatus::Unsupported(vec!["ac_power", "idle", "network"])
);
let idle_only = Require {
idle: Some("10m".into()),
..Require::default()
};
assert_eq!(
require_status(&idle_only),
RequireStatus::Unsupported(vec!["idle"])
);
let net_and_cpu = Require {
network: true,
cpu_below: Some(0.0),
..Require::default()
};
assert_eq!(
require_status(&net_and_cpu),
RequireStatus::Unsupported(vec!["network"])
);
let cpu_only = Require {
cpu_below: Some(20.0),
..Require::default()
};
assert!(unsupported_gates(&cpu_only).is_empty());
assert!(!matches!(
require_status(&cpu_only),
RequireStatus::Unsupported(_)
));
}
#[cfg(target_os = "windows")]
#[test]
fn windows_senses_every_gate() {
assert!(
unsupported_gates(&Require {
ac_power: true,
idle: Some("10m".into()),
cpu_below: Some(20.0),
network: true,
})
.is_empty()
);
}
#[test]
fn system_cpu_roundtrips_and_unknown_is_none() {
set_system_cpu(Some(42.5));
assert_eq!(system_cpu(), Some(42.5));
set_system_cpu(None);
assert!(system_cpu().is_none());
}
#[cfg(target_os = "windows")]
#[test]
fn console_idle_returns_fresh_cache_then_max_when_cleared() {
use std::time::Duration;
set_console_idle(Some(Duration::from_secs(3)));
assert_eq!(console_idle(), Some(Duration::from_secs(3)));
set_console_idle(None);
assert_eq!(console_idle(), Some(Duration::MAX));
}
}