1use std::path::{Path, PathBuf};
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17
18use serde_json::Value;
19use tokio::io::BufReader;
20use tokio::net::UnixStream;
21
22use crate::retry::{RetryPace, OS_STATE};
23use crate::state::read_state_value;
24
25pub const ARMED_STATE_MAX_AGE: Duration = Duration::from_secs(3);
27
28const RECONNECT_PACE: RetryPace = OS_STATE;
30
31pub fn vehicle_treated_as_armed(snapshot: Option<(Instant, Value)>) -> bool {
34 match snapshot {
35 Some((at, state)) if at.elapsed() <= ARMED_STATE_MAX_AGE => {
36 state.get("armed").and_then(Value::as_bool) != Some(false)
37 }
38 _ => true,
39 }
40}
41
42pub trait ArmedProbe: Send + Sync {
44 fn treated_as_armed(&self) -> bool;
46}
47
48pub struct StateSocketProbe {
50 latest: Mutex<Option<(Instant, Value)>>,
51 vehicle_profile: bool,
52}
53
54impl StateSocketProbe {
55 pub fn spawn(state_socket: PathBuf, vehicle_profile: bool) -> Arc<Self> {
60 let probe = Arc::new(Self {
61 latest: Mutex::new(None),
62 vehicle_profile,
63 });
64 if vehicle_profile {
65 let weak = Arc::downgrade(&probe);
66 tokio::spawn(async move { read_loop(state_socket, weak).await });
67 }
68 probe
69 }
70
71 fn store(&self, value: Value) {
72 if let Ok(mut latest) = self.latest.lock() {
73 *latest = Some((Instant::now(), value));
74 }
75 }
76
77 fn latest(&self) -> Option<(Instant, Value)> {
78 self.latest.lock().ok().and_then(|latest| latest.clone())
79 }
80}
81
82impl ArmedProbe for StateSocketProbe {
83 fn treated_as_armed(&self) -> bool {
84 self.vehicle_profile && vehicle_treated_as_armed(self.latest())
85 }
86}
87
88async fn read_loop(state_socket: PathBuf, probe: std::sync::Weak<StateSocketProbe>) {
89 loop {
90 if probe.strong_count() == 0 {
91 return;
92 }
93 match UnixStream::connect(&state_socket).await {
94 Ok(stream) => {
95 let mut reader = BufReader::new(stream);
96 loop {
97 match read_state_value(&mut reader).await {
98 Ok(Some(value)) => match probe.upgrade() {
99 Some(p) => p.store(value),
100 None => return,
101 },
102 Ok(None) => break,
103 Err(e) => {
104 tracing::debug!(error = %e, "armed probe: state read failed");
105 break;
106 }
107 }
108 }
109 }
110 Err(e) => {
111 tracing::debug!(
112 path = %state_socket.display(),
113 error = %e,
114 "armed probe: state socket absent; will retry"
115 );
116 }
117 }
118 tokio::time::sleep(RECONNECT_PACE.wait()).await;
119 }
120}
121
122pub async fn read_state_snapshot(
126 state_socket: &Path,
127 timeout: Duration,
128) -> Option<(Instant, Value)> {
129 let read = async {
130 let stream = UnixStream::connect(state_socket).await.ok()?;
131 let mut reader = BufReader::new(stream);
132 read_state_value(&mut reader).await.ok().flatten()
133 };
134 let value = tokio::time::timeout(timeout, read).await.ok()??;
135 Some((Instant::now(), value))
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use serde_json::json;
142
143 #[test]
144 fn an_armed_unknown_or_stale_vehicle_counts_as_armed() {
145 let now = Instant::now();
146 assert!(vehicle_treated_as_armed(None), "no state fails closed");
147 assert!(vehicle_treated_as_armed(Some((
148 now,
149 json!({"armed": true})
150 ))));
151 assert!(!vehicle_treated_as_armed(Some((
152 now,
153 json!({"armed": false})
154 ))));
155 assert!(vehicle_treated_as_armed(Some((now, json!({})))));
156 let stale = now - ARMED_STATE_MAX_AGE - Duration::from_secs(1);
157 assert!(
158 vehicle_treated_as_armed(Some((stale, json!({"armed": false})))),
159 "state older than the bound proves nothing"
160 );
161 }
162
163 #[tokio::test]
164 async fn a_probe_without_a_vehicle_never_counts_as_armed() {
165 let dir = tempfile::tempdir().unwrap();
166 let probe = StateSocketProbe::spawn(dir.path().join("state.sock"), false);
167 assert!(!probe.treated_as_armed());
168 }
169
170 #[tokio::test]
171 async fn a_vehicle_probe_with_no_state_counts_as_armed() {
172 let dir = tempfile::tempdir().unwrap();
173 let probe = StateSocketProbe::spawn(dir.path().join("state.sock"), true);
174 assert!(probe.treated_as_armed());
175 probe.store(json!({"armed": false}));
176 assert!(!probe.treated_as_armed());
177 }
178
179 #[tokio::test]
180 async fn a_missing_socket_reads_no_snapshot() {
181 let dir = tempfile::tempdir().unwrap();
182 let snap =
183 read_state_snapshot(&dir.path().join("state.sock"), Duration::from_millis(200)).await;
184 assert!(snap.is_none());
185 }
186}