use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde_json::Value;
use tokio::io::BufReader;
use tokio::net::UnixStream;
use crate::retry::{RetryPace, OS_STATE};
use crate::state::read_state_value;
pub const ARMED_STATE_MAX_AGE: Duration = Duration::from_secs(3);
const RECONNECT_PACE: RetryPace = OS_STATE;
pub fn vehicle_treated_as_armed(snapshot: Option<(Instant, Value)>) -> bool {
match snapshot {
Some((at, state)) if at.elapsed() <= ARMED_STATE_MAX_AGE => {
state.get("armed").and_then(Value::as_bool) != Some(false)
}
_ => true,
}
}
pub trait ArmedProbe: Send + Sync {
fn treated_as_armed(&self) -> bool;
}
pub struct StateSocketProbe {
latest: Mutex<Option<(Instant, Value)>>,
vehicle_profile: bool,
}
impl StateSocketProbe {
pub fn spawn(state_socket: PathBuf, vehicle_profile: bool) -> Arc<Self> {
let probe = Arc::new(Self {
latest: Mutex::new(None),
vehicle_profile,
});
if vehicle_profile {
let weak = Arc::downgrade(&probe);
tokio::spawn(async move { read_loop(state_socket, weak).await });
}
probe
}
fn store(&self, value: Value) {
if let Ok(mut latest) = self.latest.lock() {
*latest = Some((Instant::now(), value));
}
}
fn latest(&self) -> Option<(Instant, Value)> {
self.latest.lock().ok().and_then(|latest| latest.clone())
}
}
impl ArmedProbe for StateSocketProbe {
fn treated_as_armed(&self) -> bool {
self.vehicle_profile && vehicle_treated_as_armed(self.latest())
}
}
async fn read_loop(state_socket: PathBuf, probe: std::sync::Weak<StateSocketProbe>) {
loop {
if probe.strong_count() == 0 {
return;
}
match UnixStream::connect(&state_socket).await {
Ok(stream) => {
let mut reader = BufReader::new(stream);
loop {
match read_state_value(&mut reader).await {
Ok(Some(value)) => match probe.upgrade() {
Some(p) => p.store(value),
None => return,
},
Ok(None) => break,
Err(e) => {
tracing::debug!(error = %e, "armed probe: state read failed");
break;
}
}
}
}
Err(e) => {
tracing::debug!(
path = %state_socket.display(),
error = %e,
"armed probe: state socket absent; will retry"
);
}
}
tokio::time::sleep(RECONNECT_PACE.wait()).await;
}
}
pub async fn read_state_snapshot(
state_socket: &Path,
timeout: Duration,
) -> Option<(Instant, Value)> {
let read = async {
let stream = UnixStream::connect(state_socket).await.ok()?;
let mut reader = BufReader::new(stream);
read_state_value(&mut reader).await.ok().flatten()
};
let value = tokio::time::timeout(timeout, read).await.ok()??;
Some((Instant::now(), value))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn an_armed_unknown_or_stale_vehicle_counts_as_armed() {
let now = Instant::now();
assert!(vehicle_treated_as_armed(None), "no state fails closed");
assert!(vehicle_treated_as_armed(Some((
now,
json!({"armed": true})
))));
assert!(!vehicle_treated_as_armed(Some((
now,
json!({"armed": false})
))));
assert!(vehicle_treated_as_armed(Some((now, json!({})))));
let stale = now - ARMED_STATE_MAX_AGE - Duration::from_secs(1);
assert!(
vehicle_treated_as_armed(Some((stale, json!({"armed": false})))),
"state older than the bound proves nothing"
);
}
#[tokio::test]
async fn a_probe_without_a_vehicle_never_counts_as_armed() {
let dir = tempfile::tempdir().unwrap();
let probe = StateSocketProbe::spawn(dir.path().join("state.sock"), false);
assert!(!probe.treated_as_armed());
}
#[tokio::test]
async fn a_vehicle_probe_with_no_state_counts_as_armed() {
let dir = tempfile::tempdir().unwrap();
let probe = StateSocketProbe::spawn(dir.path().join("state.sock"), true);
assert!(probe.treated_as_armed());
probe.store(json!({"armed": false}));
assert!(!probe.treated_as_armed());
}
#[tokio::test]
async fn a_missing_socket_reads_no_snapshot() {
let dir = tempfile::tempdir().unwrap();
let snap =
read_state_snapshot(&dir.path().join("state.sock"), Duration::from_millis(200)).await;
assert!(snap.is_none());
}
}