use super::super::resource::pressure::{self, PressureLevel};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HostEvent {
FilesystemChanged {
path: String,
},
ProcessStarted {
pid: u32,
},
ProcessExited {
pid: u32,
exit_code: Option<i32>,
},
ResourcePressureChanged {
from: PressureLevel,
to: PressureLevel,
},
Sleep,
Wake,
NetworkChanged,
ServiceChanged {
service_id: String,
registered: Option<bool>,
running: Option<bool>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct ReconciliationState {
pub last_tick_unix_secs: Option<u64>,
pub last_pressure: Option<PressureLevel>,
}
const SLEEP_GAP_MULTIPLIER: u64 = 3;
fn now_unix_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn detect_sleep_wake(
last_tick_unix_secs: Option<u64>,
now_unix_secs: u64,
expected_interval_secs: u64,
) -> Vec<HostEvent> {
let Some(last) = last_tick_unix_secs else {
return Vec::new();
};
let gap = now_unix_secs.saturating_sub(last);
if expected_interval_secs > 0
&& gap >= expected_interval_secs.saturating_mul(SLEEP_GAP_MULTIPLIER)
{
vec![HostEvent::Sleep, HostEvent::Wake]
} else {
Vec::new()
}
}
fn detect_pressure_change(
previous: Option<PressureLevel>,
current: PressureLevel,
) -> Option<HostEvent> {
let previous = previous?;
if previous == current {
return None;
}
Some(HostEvent::ResourcePressureChanged {
from: previous,
to: current,
})
}
pub fn detect_service_change(
service_id: &str,
previous: Option<(Option<bool>, Option<bool>)>,
current: (Option<bool>, Option<bool>),
) -> Option<HostEvent> {
let previous = previous?;
if previous == current {
return None;
}
Some(HostEvent::ServiceChanged {
service_id: service_id.to_string(),
registered: current.0,
running: current.1,
})
}
pub fn reconcile(
root: &Path,
state: ReconciliationState,
expected_interval_secs: u64,
) -> (Vec<HostEvent>, ReconciliationState) {
let now = now_unix_secs();
let mut events = detect_sleep_wake(state.last_tick_unix_secs, now, expected_interval_secs);
let pressure = pressure::collect(root);
if let Some(event) = detect_pressure_change(state.last_pressure, pressure.overall) {
events.push(event);
}
let next_state = ReconciliationState {
last_tick_unix_secs: Some(now),
last_pressure: Some(pressure.overall),
};
(events, next_state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_ever_tick_reports_no_events() {
let state = ReconciliationState::default();
assert!(detect_sleep_wake(state.last_tick_unix_secs, 1000, 60).is_empty());
assert_eq!(
detect_pressure_change(state.last_pressure, PressureLevel::Normal),
None
);
}
#[test]
fn ordinary_scheduling_jitter_is_not_a_sleep_wake_cycle() {
assert!(detect_sleep_wake(Some(1000), 1065, 60).is_empty());
}
#[test]
fn a_gap_well_beyond_the_expected_interval_is_a_sleep_wake_cycle() {
let events = detect_sleep_wake(Some(1000), 1000 + 60 * 10, 60);
assert_eq!(events, vec![HostEvent::Sleep, HostEvent::Wake]);
}
#[test]
fn zero_expected_interval_never_fires_to_avoid_a_div_by_zero_style_false_positive() {
assert!(detect_sleep_wake(Some(1000), 999_999, 0).is_empty());
}
#[test]
fn pressure_change_is_reported_only_on_an_actual_transition() {
assert_eq!(
detect_pressure_change(Some(PressureLevel::Normal), PressureLevel::Critical),
Some(HostEvent::ResourcePressureChanged {
from: PressureLevel::Normal,
to: PressureLevel::Critical
})
);
assert_eq!(
detect_pressure_change(Some(PressureLevel::Normal), PressureLevel::Normal),
None
);
}
#[test]
fn service_change_is_reported_only_on_an_actual_transition() {
assert_eq!(
detect_service_change(
"giamthi",
Some((Some(true), Some(false))),
(Some(true), Some(true))
),
Some(HostEvent::ServiceChanged {
service_id: "giamthi".into(),
registered: Some(true),
running: Some(true),
})
);
assert_eq!(
detect_service_change(
"giamthi",
Some((Some(true), Some(true))),
(Some(true), Some(true))
),
None
);
}
#[test]
fn service_change_with_no_previous_reading_reports_nothing() {
assert_eq!(
detect_service_change("giamthi", None, (Some(true), Some(true))),
None
);
}
#[test]
fn reconcile_runs_against_a_real_root_without_panicking() {
let root = std::env::temp_dir();
let (events, state) = reconcile(&root, ReconciliationState::default(), 60);
assert!(events.is_empty());
assert!(state.last_tick_unix_secs.is_some());
assert!(state.last_pressure.is_some());
}
}