Rust · 10560 bytes Raw Blame History
1 use garcard_ipc::{AuthSummary, PROTOCOL_VERSION, StatusData, VersionData};
2 use std::collections::VecDeque;
3 use std::fmt;
4 use std::sync::Arc;
5 use std::sync::RwLock;
6 use std::sync::atomic::{AtomicUsize, Ordering};
7 use std::time::Instant;
8
9 /// Typed phases for auth flow transitions.
10 #[allow(dead_code)]
11 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12 pub enum AuthPhase {
13 Idle,
14 PendingPrompt,
15 Verifying,
16 Success,
17 Failure,
18 Canceled,
19 Timeout,
20 }
21
22 impl AuthPhase {
23 pub const fn as_str(self) -> &'static str {
24 match self {
25 Self::Idle => "idle",
26 Self::PendingPrompt => "pending_prompt",
27 Self::Verifying => "verifying",
28 Self::Success => "success",
29 Self::Failure => "failure",
30 Self::Canceled => "canceled",
31 Self::Timeout => "timeout",
32 }
33 }
34 }
35
36 impl fmt::Display for AuthPhase {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "{}", self.as_str())
39 }
40 }
41
42 #[allow(dead_code)]
43 fn can_transition(current: AuthPhase, next: AuthPhase) -> bool {
44 if current == next {
45 return true;
46 }
47
48 match current {
49 AuthPhase::Idle => matches!(next, AuthPhase::PendingPrompt),
50 AuthPhase::PendingPrompt => matches!(
51 next,
52 AuthPhase::Verifying | AuthPhase::Canceled | AuthPhase::Timeout
53 ),
54 AuthPhase::Verifying => matches!(
55 next,
56 AuthPhase::Success | AuthPhase::Failure | AuthPhase::Canceled | AuthPhase::Timeout
57 ),
58 AuthPhase::Success | AuthPhase::Failure | AuthPhase::Canceled | AuthPhase::Timeout => {
59 matches!(next, AuthPhase::Idle | AuthPhase::PendingPrompt)
60 }
61 }
62 }
63
64 /// Tracks auth workflow state with no sensitive data.
65 #[derive(Debug)]
66 pub struct AuthState {
67 current_phase: RwLock<AuthPhase>,
68 active_requests: AtomicUsize,
69 queued_requests: AtomicUsize,
70 }
71
72 impl Default for AuthState {
73 fn default() -> Self {
74 Self {
75 current_phase: RwLock::new(AuthPhase::Idle),
76 active_requests: AtomicUsize::new(0),
77 queued_requests: AtomicUsize::new(0),
78 }
79 }
80 }
81
82 impl AuthState {
83 pub fn phase(&self) -> AuthPhase {
84 self.current_phase
85 .read()
86 .map(|phase| *phase)
87 .unwrap_or(AuthPhase::Idle)
88 }
89
90 pub fn set_phase(&self, next: AuthPhase) {
91 if let Ok(mut phase) = self.current_phase.write() {
92 *phase = next;
93 }
94 }
95
96 #[allow(dead_code)]
97 pub fn transition(&self, next: AuthPhase) -> bool {
98 if let Ok(mut phase) = self.current_phase.write() {
99 if can_transition(*phase, next) {
100 *phase = next;
101 return true;
102 }
103 return false;
104 }
105
106 false
107 }
108
109 pub fn set_active_requests(&self, count: usize) {
110 self.active_requests.store(count, Ordering::Relaxed);
111 }
112
113 pub fn set_queued_requests(&self, count: usize) {
114 self.queued_requests.store(count, Ordering::Relaxed);
115 }
116
117 pub fn sync_queue_counts(&self, active: usize, queued: usize) {
118 self.set_active_requests(active);
119 self.set_queued_requests(queued);
120 }
121
122 pub fn summary(&self) -> AuthSummary {
123 AuthSummary {
124 state: self.phase().to_string(),
125 active_requests: self.active_requests.load(Ordering::Relaxed),
126 queued_requests: self.queued_requests.load(Ordering::Relaxed),
127 }
128 }
129 }
130
131 /// Queue policy for concurrent auth requests: one active request and FIFO backlog.
132 #[derive(Debug, Clone)]
133 pub struct AuthQueue<T> {
134 active: Option<T>,
135 queued: VecDeque<T>,
136 }
137
138 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
139 pub enum QueueInsert {
140 Activated,
141 Queued { position: usize },
142 }
143
144 impl<T> Default for AuthQueue<T> {
145 fn default() -> Self {
146 Self {
147 active: None,
148 queued: VecDeque::new(),
149 }
150 }
151 }
152
153 impl<T> AuthQueue<T> {
154 pub fn push(&mut self, request: T) -> QueueInsert {
155 if self.active.is_none() {
156 self.active = Some(request);
157 QueueInsert::Activated
158 } else {
159 self.queued.push_back(request);
160 QueueInsert::Queued {
161 position: self.queued.len(),
162 }
163 }
164 }
165
166 pub fn active(&self) -> Option<&T> {
167 self.active.as_ref()
168 }
169
170 pub fn take_active_if<F>(&mut self, mut predicate: F) -> Option<T>
171 where
172 F: FnMut(&T) -> bool,
173 {
174 if self.active.as_ref().is_some_and(&mut predicate) {
175 return self.complete_active();
176 }
177
178 None
179 }
180
181 pub fn complete_active_if<F>(&mut self, mut predicate: F) -> Option<T>
182 where
183 F: FnMut(&T) -> bool,
184 {
185 if self.active.as_ref().is_some_and(&mut predicate) {
186 return self.complete_active();
187 }
188
189 None
190 }
191
192 pub fn remove_queued_if<F>(&mut self, mut predicate: F) -> bool
193 where
194 F: FnMut(&T) -> bool,
195 {
196 if let Some(index) = self.queued.iter().position(&mut predicate) {
197 self.queued.remove(index);
198 return true;
199 }
200
201 false
202 }
203
204 pub fn complete_active(&mut self) -> Option<T> {
205 let finished = self.active.take();
206 self.promote_next();
207 finished
208 }
209
210 pub fn active_len(&self) -> usize {
211 usize::from(self.active.is_some())
212 }
213
214 pub fn queued_len(&self) -> usize {
215 self.queued.len()
216 }
217
218 #[allow(dead_code)]
219 pub fn is_empty(&self) -> bool {
220 self.active.is_none() && self.queued.is_empty()
221 }
222
223 pub fn clear(&mut self) {
224 self.active = None;
225 self.queued.clear();
226 }
227
228 pub fn counts(&self) -> (usize, usize) {
229 (self.active_len(), self.queued_len())
230 }
231
232 fn promote_next(&mut self) {
233 if self.active.is_none() {
234 self.active = self.queued.pop_front();
235 }
236 }
237 }
238
239 /// Immutable process/runtime metadata for IPC status.
240 #[derive(Debug)]
241 pub struct RuntimeState {
242 started_at: Instant,
243 pid: u32,
244 socket_path: String,
245 backend_name: &'static str,
246 auth: Arc<AuthState>,
247 }
248
249 impl RuntimeState {
250 #[cfg(test)]
251 pub fn new(socket_path: String, backend_name: &'static str) -> Self {
252 Self::with_auth(socket_path, backend_name, Arc::new(AuthState::default()))
253 }
254
255 pub fn with_auth(
256 socket_path: String,
257 backend_name: &'static str,
258 auth: Arc<AuthState>,
259 ) -> Self {
260 auth.set_phase(AuthPhase::Idle);
261 auth.set_active_requests(0);
262 auth.set_queued_requests(0);
263 Self {
264 started_at: Instant::now(),
265 pid: std::process::id(),
266 socket_path,
267 backend_name,
268 auth,
269 }
270 }
271
272 pub fn status(&self) -> StatusData {
273 StatusData {
274 running: true,
275 pid: self.pid,
276 uptime_secs: self.started_at.elapsed().as_secs(),
277 version: env!("CARGO_PKG_VERSION").to_string(),
278 protocol_version: PROTOCOL_VERSION,
279 socket_path: self.socket_path.clone(),
280 agent_backend: self.backend_name.to_string(),
281 }
282 }
283
284 pub fn version(&self) -> VersionData {
285 VersionData {
286 component: "garcard".to_string(),
287 version: env!("CARGO_PKG_VERSION").to_string(),
288 protocol_version: PROTOCOL_VERSION,
289 }
290 }
291
292 pub fn auth_summary(&self) -> AuthSummary {
293 self.auth.summary()
294 }
295
296 #[allow(dead_code)]
297 pub fn auth_mutation(&self) -> &Arc<AuthState> {
298 &self.auth
299 }
300 }
301
302 #[cfg(test)]
303 mod tests {
304 use super::*;
305
306 #[test]
307 fn auth_state_defaults_to_idle_phase() {
308 let state = AuthState::default();
309 let summary = state.summary();
310 assert_eq!(summary.state, "idle");
311 assert_eq!(summary.active_requests, 0);
312 assert_eq!(summary.queued_requests, 0);
313 }
314
315 #[test]
316 fn auth_state_updates_summary() {
317 let state = AuthState::default();
318 state.set_phase(AuthPhase::Verifying);
319 state.set_active_requests(2);
320 state.set_queued_requests(3);
321
322 let summary = state.summary();
323 assert_eq!(summary.state, "verifying");
324 assert_eq!(summary.active_requests, 2);
325 assert_eq!(summary.queued_requests, 3);
326 }
327
328 #[test]
329 fn auth_phase_transition_rules_are_enforced() {
330 let state = AuthState::default();
331 assert!(state.transition(AuthPhase::PendingPrompt));
332 assert!(state.transition(AuthPhase::Verifying));
333 assert!(state.transition(AuthPhase::Failure));
334 assert!(state.transition(AuthPhase::Idle));
335 assert!(!state.transition(AuthPhase::Verifying));
336 assert_eq!(state.phase(), AuthPhase::Idle);
337 }
338
339 #[test]
340 fn auth_queue_activates_then_queues() {
341 let mut queue = AuthQueue::default();
342 assert_eq!(queue.push(10), QueueInsert::Activated);
343 assert_eq!(queue.push(20), QueueInsert::Queued { position: 1 });
344 assert_eq!(queue.push(30), QueueInsert::Queued { position: 2 });
345 assert_eq!(queue.active(), Some(&10));
346 assert_eq!(queue.counts(), (1, 2));
347 }
348
349 #[test]
350 fn auth_queue_completion_promotes_next_request() {
351 let mut queue = AuthQueue::default();
352 queue.push("first");
353 queue.push("second");
354 queue.push("third");
355
356 assert_eq!(queue.complete_active(), Some("first"));
357 assert_eq!(queue.active(), Some(&"second"));
358 assert_eq!(queue.complete_active(), Some("second"));
359 assert_eq!(queue.active(), Some(&"third"));
360 assert_eq!(queue.complete_active(), Some("third"));
361 assert!(queue.is_empty());
362 }
363
364 #[test]
365 fn auth_queue_take_active_if_promotes_next() {
366 let mut queue = AuthQueue::default();
367 queue.push("cookie-a");
368 queue.push("cookie-b");
369
370 let removed = queue.take_active_if(|value| *value == "cookie-a");
371 assert_eq!(removed, Some("cookie-a"));
372 assert_eq!(queue.active(), Some(&"cookie-b"));
373 }
374
375 #[test]
376 fn auth_queue_remove_queued_if_removes_specific_item() {
377 let mut queue = AuthQueue::default();
378 queue.push("cookie-a");
379 queue.push("cookie-b");
380 queue.push("cookie-c");
381
382 assert!(queue.remove_queued_if(|value| *value == "cookie-b"));
383 assert_eq!(queue.counts(), (1, 1));
384 assert!(!queue.remove_queued_if(|value| *value == "missing"));
385 }
386 }
387