use std::time::Duration;
use kanade_shared::ipc::state::{Check, CheckStatus, StateSnapshot};
use kanade_shared::wire::EffectiveConfig;
use tokio::sync::watch;
use tracing::debug;
const EVAL_INTERVAL: Duration = Duration::from_secs(30);
pub fn eval_once(pc_id: &str, agent_version: &str, cfg: &EffectiveConfig) -> StateSnapshot {
let checks = vec![
agent_self_update_check(agent_version, cfg.target_version.as_deref()),
disk_free_check(),
bitlocker_check_stub(),
av_signature_check_stub(),
cert_expiry_check_stub(),
];
StateSnapshot {
pc_id: pc_id.to_string(),
online: true,
vpn: "unknown".to_string(),
checks,
agent_version: agent_version.to_string(),
target_version: cfg
.target_version
.as_deref()
.filter(|s| !s.is_empty())
.map(str::to_owned)
.unwrap_or_else(|| agent_version.to_string()),
}
}
pub async fn eval_loop(
state_tx: watch::Sender<StateSnapshot>,
cfg_rx: watch::Receiver<EffectiveConfig>,
pc_id: String,
agent_version: String,
) {
let mut tick = tokio::time::interval(EVAL_INTERVAL);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
tick.tick().await;
loop {
tick.tick().await;
let snapshot = eval_once(&pc_id, &agent_version, &cfg_rx.borrow());
if state_tx.send(snapshot).is_err() {
debug!(pc_id = %pc_id, "state.eval_loop: no receivers, exiting");
return;
}
}
}
fn agent_self_update_check(running: &str, target: Option<&str>) -> Check {
let target = target.filter(|s| !s.is_empty()).unwrap_or(running);
if running == target {
Check {
name: "agent_self_update".into(),
status: CheckStatus::Ok,
detail: Some(format!("running {running} (target matches)")),
troubleshoot: None,
}
} else {
Check {
name: "agent_self_update".into(),
status: CheckStatus::Warn,
detail: Some(format!(
"running {running}, target {target} — restart pending"
)),
troubleshoot: None,
}
}
}
#[cfg(target_os = "windows")]
fn disk_free_check() -> Check {
use windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
use windows::core::w;
let mut free: u64 = 0;
let mut total: u64 = 0;
let result =
unsafe { GetDiskFreeSpaceExW(w!("C:\\"), None, Some(&mut total), Some(&mut free)) };
if let Err(e) = result {
return Check {
name: "disk_free".into(),
status: CheckStatus::Unknown,
detail: Some(format!("GetDiskFreeSpaceExW failed: {e}")),
troubleshoot: None,
};
}
if total == 0 {
return Check {
name: "disk_free".into(),
status: CheckStatus::Unknown,
detail: Some("C:\\ reports 0 total bytes".into()),
troubleshoot: None,
};
}
let pct = (free as f64 / total as f64) * 100.0;
let to_gb = |b: u64| (b as f64) / 1024.0 / 1024.0 / 1024.0;
let detail = Some(format!(
"{:.1}% free ({:.1} GB / {:.1} GB)",
pct,
to_gb(free),
to_gb(total),
));
let status = if pct >= 10.0 {
CheckStatus::Ok
} else if pct >= 5.0 {
CheckStatus::Warn
} else {
CheckStatus::Fail
};
Check {
name: "disk_free".into(),
status,
detail,
troubleshoot: None,
}
}
#[cfg(not(target_os = "windows"))]
fn disk_free_check() -> Check {
Check {
name: "disk_free".into(),
status: CheckStatus::Unknown,
detail: Some("disk_free not implemented on non-Windows targets".into()),
troubleshoot: None,
}
}
fn bitlocker_check_stub() -> Check {
Check {
name: "bitlocker".into(),
status: CheckStatus::Unknown,
detail: Some("TODO: implement via Win32_EncryptableVolume WMI query".into()),
troubleshoot: None,
}
}
fn av_signature_check_stub() -> Check {
Check {
name: "av_signature".into(),
status: CheckStatus::Unknown,
detail: Some("TODO: implement via MSFT_MpComputerStatus".into()),
troubleshoot: None,
}
}
fn cert_expiry_check_stub() -> Check {
Check {
name: "cert_expiry".into(),
status: CheckStatus::Unknown,
detail: Some("TODO: implement via wincrypt cert-store enumeration".into()),
troubleshoot: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg_with(target: Option<&str>) -> EffectiveConfig {
let mut cfg = EffectiveConfig::builtin_defaults();
cfg.target_version = target.map(str::to_owned);
cfg
}
#[test]
fn eval_once_produces_well_formed_snapshot() {
let snap = eval_once("PC1234", "0.41.0", &cfg_with(None));
assert_eq!(snap.pc_id, "PC1234");
assert!(snap.online);
assert_eq!(snap.vpn, "unknown");
assert_eq!(snap.agent_version, "0.41.0");
assert_eq!(snap.target_version, "0.41.0"); assert_eq!(snap.checks.len(), 5);
let names: Vec<&str> = snap.checks.iter().map(|c| c.name.as_str()).collect();
assert_eq!(
names,
vec![
"agent_self_update",
"disk_free",
"bitlocker",
"av_signature",
"cert_expiry"
]
);
}
#[test]
fn agent_self_update_ok_when_running_matches_target() {
let c = agent_self_update_check("0.41.0", Some("0.41.0"));
assert_eq!(c.status, CheckStatus::Ok);
}
#[test]
fn agent_self_update_ok_when_target_unset() {
let c = agent_self_update_check("0.41.0", None);
assert_eq!(c.status, CheckStatus::Ok);
}
#[test]
fn agent_self_update_ok_when_target_empty_string() {
let c = agent_self_update_check("0.41.0", Some(""));
assert_eq!(c.status, CheckStatus::Ok);
}
#[test]
fn agent_self_update_warn_when_target_differs() {
let c = agent_self_update_check("0.41.0", Some("0.42.0"));
assert_eq!(c.status, CheckStatus::Warn);
assert!(c.detail.unwrap().contains("restart pending"));
}
#[cfg(target_os = "windows")]
#[test]
fn disk_free_returns_concrete_status_on_windows() {
let c = disk_free_check();
assert_eq!(c.name, "disk_free");
assert!(
matches!(
c.status,
CheckStatus::Ok | CheckStatus::Warn | CheckStatus::Fail
),
"expected concrete status, got {:?}",
c.status
);
let detail = c.detail.expect("detail populated");
assert!(detail.contains("free"), "detail: {detail}");
}
#[test]
fn snapshot_target_version_falls_back_when_cfg_target_empty() {
let snap = eval_once("PC1234", "0.41.0", &cfg_with(Some("")));
assert_eq!(snap.target_version, "0.41.0");
}
#[test]
fn snapshot_target_version_surfaces_when_cfg_target_set() {
let snap = eval_once("PC1234", "0.41.0", &cfg_with(Some("0.42.0")));
assert_eq!(snap.target_version, "0.42.0");
}
}