close sprint two blockers
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
37b841dc7b138df51b29d45d034f2396f6e2caa9- Parents
-
617b876 - Tree
eb65dc2
37b841d
37b841dc7b138df51b29d45d034f2396f6e2caa9617b876
eb65dc2| Status | File | + | - |
|---|---|---|---|
| M |
README.md
|
1 | 0 |
| M |
examples/config.toml
|
4 | 0 |
| A |
examples/sprint-02-validation-report-2026-02-18.md
|
46 | 0 |
| M |
examples/sprint-02-validation.md
|
17 | 0 |
| M |
examples/validate-sprint-02.sh
|
14 | 10 |
| M |
garcard/src/agent.rs
|
110 | 19 |
| M |
garcard/src/config.rs
|
39 | 0 |
| M |
garcard/src/daemon.rs
|
67 | 0 |
README.mdmodified@@ -25,6 +25,7 @@ Environment overrides: | ||
| 25 | 25 | 7. `GARCARD_POLKIT_HELPER_SOCKET` |
| 26 | 26 | 8. `GARCARD_PROMPT_COMMAND` |
| 27 | 27 | 9. `GARCARD_PROMPT_TIMEOUT_SECS` |
| 28 | +10. `GARCARD_BACKEND_HEALTHCHECK_SECS` | |
| 28 | 29 | |
| 29 | 30 | See `examples/config.toml` for a starter file. |
| 30 | 31 | |
examples/config.tomlmodified@@ -20,3 +20,7 @@ polkit_object_path = "/org/gardesk/Garcard/AuthAgent" | ||
| 20 | 20 | |
| 21 | 21 | # Locale sent to polkit authority registration. |
| 22 | 22 | locale = "en_US.UTF-8" |
| 23 | + | |
| 24 | +# Backend health check interval in seconds. | |
| 25 | +# Failed checks trigger a reconnect attempt for the selected auth backend. | |
| 26 | +backend_healthcheck_secs = "5" | |
examples/sprint-02-validation-report-2026-02-18.mdadded@@ -0,0 +1,46 @@ | ||
| 1 | +# Sprint 02 Validation Report (2026-02-18) | |
| 2 | + | |
| 3 | +## Environment | |
| 4 | +1. Host: `mizu` (NixOS) | |
| 5 | +2. Daemon run mode: `GARCARD_AGENT_BACKEND=polkit` | |
| 6 | +3. Socket: `/run/user/1000/garcard.sock` | |
| 7 | + | |
| 8 | +## Baseline Checks | |
| 9 | +1. `garcardctl ping` -> success. | |
| 10 | +2. `garcardctl status` -> running with `agent_backend: polkit`. | |
| 11 | +3. `garcardctl auth-summary` -> `idle`. | |
| 12 | + | |
| 13 | +## Live Policy Checks | |
| 14 | +1. `pkcheck --allow-user-interaction --process $$ --action-id org.freedesktop.login1.power-off` | |
| 15 | +Result: | |
| 16 | +1. Exit code `0`. | |
| 17 | +2. No challenge emitted (policy already authorized in this session context). | |
| 18 | + | |
| 19 | +2. `pkcheck --allow-user-interaction --process $$ --action-id com.mesonbuild.install.run` | |
| 20 | +Result: | |
| 21 | +1. Exit code `1` (`Not authorized.`). | |
| 22 | +2. Daemon logs showed live challenge callback: | |
| 23 | + - `Started active polkit auth request ...` | |
| 24 | + - `Processing polkit auth request ...` | |
| 25 | + - `Starting helper authentication dialog ...` | |
| 26 | +3. `garcardctl auth-summary` after call -> `timeout`. | |
| 27 | + | |
| 28 | +## Reconnect Validation | |
| 29 | +1. `kill -HUP <garcard-pid>` (forced reconnect path). | |
| 30 | +2. Daemon logs: | |
| 31 | + - `Received SIGHUP; forcing backend reconnect` | |
| 32 | + - `Unregistered polkit authentication agent` | |
| 33 | + - `Registered polkit authentication agent` | |
| 34 | +3. Post-check: | |
| 35 | + - `garcardctl status` remained responsive. | |
| 36 | + - `garcardctl auth-summary` remained consistent. | |
| 37 | + | |
| 38 | +## Optional Root-Level Disruption Check | |
| 39 | +1. Attempted `systemctl restart polkit`. | |
| 40 | +2. Result: `Access denied` (no root/system permission from this validation context). | |
| 41 | +3. Root-level service restart scenario remains for host-owner execution if desired. | |
| 42 | + | |
| 43 | +## Conclusion | |
| 44 | +1. Sprint 02 auth callback path is live and receiving real polkit challenge requests. | |
| 45 | +2. Timeout state is observable and propagated through IPC summary. | |
| 46 | +3. Backend reconnect behavior is validated through forced reconnect workflow. | |
examples/sprint-02-validation.mdmodified@@ -58,3 +58,20 @@ Expected: | ||
| 58 | 58 | 1. One active request at a time. |
| 59 | 59 | 2. Additional requests queue and process FIFO. |
| 60 | 60 | 3. No deadlock after cancel/failure/success. |
| 61 | + | |
| 62 | +## Backend Reconnect (Bus/Authority Disruption) | |
| 63 | +1. Start daemon with visible logs: | |
| 64 | + - `RUST_LOG=garcard=debug cargo run -p garcard -- daemon` | |
| 65 | +2. In another terminal, capture status: | |
| 66 | + - `cargo run -q -p garcardctl -- status` | |
| 67 | +3. Force reconnect path without root: | |
| 68 | + - `kill -HUP <garcard-pid>` | |
| 69 | +4. Wait at least one health interval (default 5s), then check status and logs. | |
| 70 | +5. Optional root-level disruption check: | |
| 71 | + - `sudo systemctl restart polkit` | |
| 72 | + | |
| 73 | +Expected: | |
| 74 | +1. Log shows forced reconnect path (`Received SIGHUP; forcing backend reconnect`). | |
| 75 | +2. Backend re-registers without daemon process restart. | |
| 76 | +3. `garcardctl status` remains responsive during/after reconnect attempt. | |
| 77 | +4. Optional root-level disruption should trigger maintenance reconnect attempts. | |
examples/validate-sprint-02.shmodified@@ -3,25 +3,29 @@ set -euo pipefail | ||
| 3 | 3 | |
| 4 | 4 | ACTION_ID="${1:-org.freedesktop.login1.power-off}" |
| 5 | 5 | |
| 6 | -if ! command -v garcardctl >/dev/null 2>&1; then | |
| 7 | - echo "garcardctl not found in PATH" | |
| 8 | - echo "Run from this repo with: cargo run -p garcardctl -- <command>" | |
| 9 | - exit 1 | |
| 10 | -fi | |
| 11 | - | |
| 12 | 6 | if ! command -v pkcheck >/dev/null 2>&1; then |
| 13 | 7 | echo "pkcheck not found; install polkit tools to run live auth validation" |
| 14 | 8 | exit 1 |
| 15 | 9 | fi |
| 16 | 10 | |
| 11 | +if command -v garcardctl >/dev/null 2>&1; then | |
| 12 | + GARCARDCTL=(garcardctl) | |
| 13 | +else | |
| 14 | + GARCARDCTL=(cargo run -q -p garcardctl --) | |
| 15 | +fi | |
| 16 | + | |
| 17 | +run_garcardctl() { | |
| 18 | + "${GARCARDCTL[@]}" "$@" | |
| 19 | +} | |
| 20 | + | |
| 17 | 21 | echo "[1/5] Check daemon connectivity" |
| 18 | -garcardctl ping | |
| 22 | +run_garcardctl ping | |
| 19 | 23 | |
| 20 | 24 | echo "[2/5] Check daemon status" |
| 21 | -garcardctl status | |
| 25 | +run_garcardctl status | |
| 22 | 26 | |
| 23 | 27 | echo "[3/5] Check pre-auth summary" |
| 24 | -garcardctl auth-summary | |
| 28 | +run_garcardctl auth-summary | |
| 25 | 29 | |
| 26 | 30 | echo "[4/5] Trigger interactive policy check" |
| 27 | 31 | echo "Action ID: ${ACTION_ID}" |
@@ -33,7 +37,7 @@ set -e | ||
| 33 | 37 | echo "pkcheck exit code: ${PKCHECK_RC}" |
| 34 | 38 | |
| 35 | 39 | echo "[5/5] Check post-auth summary" |
| 36 | -garcardctl auth-summary | |
| 40 | +run_garcardctl auth-summary | |
| 37 | 41 | |
| 38 | 42 | cat <<'EOF' |
| 39 | 43 | Exit code hints: |
garcard/src/agent.rsmodified@@ -16,6 +16,9 @@ pub trait AuthAgentBackend { | ||
| 16 | 16 | fn name(&self) -> &'static str; |
| 17 | 17 | fn register(&mut self) -> Result<()>; |
| 18 | 18 | fn unregister(&mut self) -> Result<()>; |
| 19 | + fn maintain(&mut self) -> Result<()> { | |
| 20 | + Ok(()) | |
| 21 | + } | |
| 19 | 22 | } |
| 20 | 23 | |
| 21 | 24 | /// Placeholder backend used during Sprint 01 scaffolding. |
@@ -472,6 +475,21 @@ impl PolkitAgent { | ||
| 472 | 475 | )?; |
| 473 | 476 | Ok(proxy) |
| 474 | 477 | } |
| 478 | + | |
| 479 | + fn peer_proxy(connection: &Connection) -> Result<Proxy<'_>> { | |
| 480 | + let proxy = Proxy::new( | |
| 481 | + connection, | |
| 482 | + "org.freedesktop.PolicyKit1", | |
| 483 | + "/org/freedesktop/PolicyKit1/Authority", | |
| 484 | + "org.freedesktop.DBus.Peer", | |
| 485 | + )?; | |
| 486 | + Ok(proxy) | |
| 487 | + } | |
| 488 | + | |
| 489 | + fn ping_authority(connection: &Connection) -> Result<()> { | |
| 490 | + let _: () = Self::peer_proxy(connection)?.call("Ping", &())?; | |
| 491 | + Ok(()) | |
| 492 | + } | |
| 475 | 493 | } |
| 476 | 494 | |
| 477 | 495 | impl AuthAgentBackend for PolkitAgent { |
@@ -506,7 +524,7 @@ impl AuthAgentBackend for PolkitAgent { | ||
| 506 | 524 | &( |
| 507 | 525 | &self.subject, |
| 508 | 526 | self.locale.as_str(), |
| 509 | - self.object_path.clone(), | |
| 527 | + self.object_path.to_string(), | |
| 510 | 528 | ), |
| 511 | 529 | )?; |
| 512 | 530 | Ok(()) |
@@ -534,22 +552,29 @@ impl AuthAgentBackend for PolkitAgent { | ||
| 534 | 552 | } |
| 535 | 553 | |
| 536 | 554 | if let Some(connection) = &self.connection { |
| 537 | - match Self::proxy(connection)?.call::<_, _, ()>( | |
| 538 | - "UnregisterAuthenticationAgent", | |
| 539 | - &( | |
| 540 | - &self.subject, | |
| 541 | - self.locale.as_str(), | |
| 542 | - self.object_path.clone(), | |
| 543 | - ), | |
| 544 | - ) { | |
| 545 | - Ok(()) => { | |
| 546 | - tracing::info!( | |
| 547 | - backend = self.name(), | |
| 548 | - "Unregistered polkit authentication agent" | |
| 549 | - ); | |
| 550 | - } | |
| 555 | + match Self::proxy(connection) { | |
| 556 | + Ok(proxy) => match proxy.call::<_, _, ()>( | |
| 557 | + "UnregisterAuthenticationAgent", | |
| 558 | + &(&self.subject, self.object_path.to_string()), | |
| 559 | + ) { | |
| 560 | + Ok(()) => { | |
| 561 | + tracing::info!( | |
| 562 | + backend = self.name(), | |
| 563 | + "Unregistered polkit authentication agent" | |
| 564 | + ); | |
| 565 | + } | |
| 566 | + Err(err) => { | |
| 567 | + tracing::warn!( | |
| 568 | + error = %err, | |
| 569 | + "Failed to unregister polkit authentication agent" | |
| 570 | + ); | |
| 571 | + } | |
| 572 | + }, | |
| 551 | 573 | Err(err) => { |
| 552 | - tracing::warn!(error = %err, "Failed to unregister polkit authentication agent"); | |
| 574 | + tracing::warn!( | |
| 575 | + error = %err, | |
| 576 | + "Failed to create polkit authority proxy during unregister" | |
| 577 | + ); | |
| 553 | 578 | } |
| 554 | 579 | } |
| 555 | 580 | |
@@ -566,18 +591,78 @@ impl AuthAgentBackend for PolkitAgent { | ||
| 566 | 591 | self.connection = None; |
| 567 | 592 | Ok(()) |
| 568 | 593 | } |
| 594 | + | |
| 595 | + fn maintain(&mut self) -> Result<()> { | |
| 596 | + if !self.registered { | |
| 597 | + return self.register(); | |
| 598 | + } | |
| 599 | + | |
| 600 | + let is_healthy = self | |
| 601 | + .connection | |
| 602 | + .as_ref() | |
| 603 | + .map(|connection| Self::ping_authority(connection).is_ok()) | |
| 604 | + .unwrap_or(false); | |
| 605 | + if is_healthy { | |
| 606 | + return Ok(()); | |
| 607 | + } | |
| 608 | + | |
| 609 | + tracing::warn!( | |
| 610 | + backend = self.name(), | |
| 611 | + "Polkit backend health check failed; attempting reconnect" | |
| 612 | + ); | |
| 613 | + let _ = self.unregister(); | |
| 614 | + self.register() | |
| 615 | + } | |
| 569 | 616 | } |
| 570 | 617 | |
| 571 | 618 | fn build_subject() -> Subject { |
| 619 | + if let Some(session_id) = current_session_id() { | |
| 620 | + let mut details = HashMap::new(); | |
| 621 | + let value = zbus::zvariant::Value::from(session_id.as_str()); | |
| 622 | + if let Ok(session_value) = OwnedValue::try_from(value) { | |
| 623 | + details.insert("session-id".to_string(), session_value); | |
| 624 | + return ("unix-session".to_string(), details); | |
| 625 | + } | |
| 626 | + } | |
| 627 | + | |
| 572 | 628 | let mut details = HashMap::new(); |
| 573 | 629 | details.insert("pid".to_string(), OwnedValue::from(std::process::id())); |
| 574 | 630 | details.insert( |
| 575 | 631 | "uid".to_string(), |
| 576 | 632 | OwnedValue::from(nix::unistd::geteuid().as_raw()), |
| 577 | 633 | ); |
| 634 | + if let Some(start_time) = process_start_time_ticks() { | |
| 635 | + details.insert("start-time".to_string(), OwnedValue::from(start_time)); | |
| 636 | + } else { | |
| 637 | + tracing::warn!("Unable to determine process start-time for polkit subject"); | |
| 638 | + } | |
| 578 | 639 | ("unix-process".to_string(), details) |
| 579 | 640 | } |
| 580 | 641 | |
| 642 | +fn process_start_time_ticks() -> Option<u64> { | |
| 643 | + let stat = std::fs::read_to_string("/proc/self/stat").ok()?; | |
| 644 | + let (_head, tail) = stat.rsplit_once(") ")?; | |
| 645 | + let mut fields = tail.split_whitespace(); | |
| 646 | + fields.nth(19)?.parse::<u64>().ok() | |
| 647 | +} | |
| 648 | + | |
| 649 | +fn current_session_id() -> Option<String> { | |
| 650 | + if let Ok(raw) = std::env::var("XDG_SESSION_ID") { | |
| 651 | + let trimmed = raw.trim(); | |
| 652 | + if !trimmed.is_empty() { | |
| 653 | + return Some(trimmed.to_string()); | |
| 654 | + } | |
| 655 | + } | |
| 656 | + | |
| 657 | + let raw = std::fs::read_to_string("/proc/self/sessionid").ok()?; | |
| 658 | + let trimmed = raw.trim(); | |
| 659 | + if trimmed.is_empty() || trimmed == "4294967295" { | |
| 660 | + return None; | |
| 661 | + } | |
| 662 | + | |
| 663 | + Some(trimmed.to_string()) | |
| 664 | +} | |
| 665 | + | |
| 581 | 666 | #[cfg(test)] |
| 582 | 667 | mod tests { |
| 583 | 668 | use super::*; |
@@ -608,9 +693,15 @@ mod tests { | ||
| 608 | 693 | #[test] |
| 609 | 694 | fn subject_uses_unix_process_kind() { |
| 610 | 695 | let subject = build_subject(); |
| 611 | - assert_eq!(subject.0, "unix-process"); | |
| 612 | - assert!(subject.1.contains_key("pid")); | |
| 613 | - assert!(subject.1.contains_key("uid")); | |
| 696 | + match subject.0.as_str() { | |
| 697 | + "unix-session" => assert!(subject.1.contains_key("session-id")), | |
| 698 | + "unix-process" => { | |
| 699 | + assert!(subject.1.contains_key("pid")); | |
| 700 | + assert!(subject.1.contains_key("uid")); | |
| 701 | + assert!(subject.1.contains_key("start-time")); | |
| 702 | + } | |
| 703 | + other => panic!("unexpected subject kind: {other}"), | |
| 704 | + } | |
| 614 | 705 | } |
| 615 | 706 | |
| 616 | 707 | #[test] |
garcard/src/config.rsmodified@@ -12,6 +12,7 @@ pub struct Config { | ||
| 12 | 12 | pub agent_backend: AgentBackendMode, |
| 13 | 13 | pub polkit_object_path: String, |
| 14 | 14 | pub locale: String, |
| 15 | + pub backend_healthcheck_secs: u64, | |
| 15 | 16 | } |
| 16 | 17 | |
| 17 | 18 | impl Default for Config { |
@@ -22,6 +23,7 @@ impl Default for Config { | ||
| 22 | 23 | agent_backend: AgentBackendMode::Auto, |
| 23 | 24 | polkit_object_path: "/org/gardesk/Garcard/AuthAgent".to_string(), |
| 24 | 25 | locale: std::env::var("LANG").unwrap_or_else(|_| "en_US.UTF-8".to_string()), |
| 26 | + backend_healthcheck_secs: 5, | |
| 25 | 27 | } |
| 26 | 28 | } |
| 27 | 29 | } |
@@ -50,6 +52,12 @@ impl Config { | ||
| 50 | 52 | if let Some(raw_locale) = std::env::var_os("GARCARD_LOCALE") { |
| 51 | 53 | cfg.locale = raw_locale.to_string_lossy().to_string(); |
| 52 | 54 | } |
| 55 | + if let Some(raw_interval) = std::env::var_os("GARCARD_BACKEND_HEALTHCHECK_SECS") { | |
| 56 | + cfg.backend_healthcheck_secs = parse_positive_secs(&raw_interval.to_string_lossy()) | |
| 57 | + .with_context(|| { | |
| 58 | + "Invalid GARCARD_BACKEND_HEALTHCHECK_SECS (expected positive integer seconds)" | |
| 59 | + })?; | |
| 60 | + } | |
| 53 | 61 | |
| 54 | 62 | Ok(cfg) |
| 55 | 63 | } |
@@ -84,6 +92,12 @@ impl Config { | ||
| 84 | 92 | if let Some(locale) = file_cfg.locale { |
| 85 | 93 | self.locale = locale; |
| 86 | 94 | } |
| 95 | + if let Some(backend_healthcheck_secs) = file_cfg.backend_healthcheck_secs { | |
| 96 | + self.backend_healthcheck_secs = parse_positive_secs(&backend_healthcheck_secs) | |
| 97 | + .with_context(|| { | |
| 98 | + "Invalid backend_healthcheck_secs in config file (expected positive integer seconds)" | |
| 99 | + })?; | |
| 100 | + } | |
| 87 | 101 | |
| 88 | 102 | Ok(()) |
| 89 | 103 | } |
@@ -126,6 +140,7 @@ struct FileConfig { | ||
| 126 | 140 | agent_backend: Option<String>, |
| 127 | 141 | polkit_object_path: Option<String>, |
| 128 | 142 | locale: Option<String>, |
| 143 | + backend_healthcheck_secs: Option<String>, | |
| 129 | 144 | } |
| 130 | 145 | |
| 131 | 146 | fn config_path() -> Option<PathBuf> { |
@@ -143,6 +158,17 @@ fn parse_octal_mode(raw: &str) -> Result<u32> { | ||
| 143 | 158 | Ok(mode) |
| 144 | 159 | } |
| 145 | 160 | |
| 161 | +fn parse_positive_secs(raw: &str) -> Result<u64> { | |
| 162 | + let trimmed = raw.trim(); | |
| 163 | + let secs = trimmed | |
| 164 | + .parse::<u64>() | |
| 165 | + .with_context(|| format!("failed to parse seconds value: {trimmed}"))?; | |
| 166 | + if secs == 0 { | |
| 167 | + anyhow::bail!("seconds must be greater than zero"); | |
| 168 | + } | |
| 169 | + Ok(secs) | |
| 170 | +} | |
| 171 | + | |
| 146 | 172 | #[cfg(test)] |
| 147 | 173 | mod tests { |
| 148 | 174 | use super::*; |
@@ -168,6 +194,7 @@ socket_mode = "640" | ||
| 168 | 194 | agent_backend = "stub" |
| 169 | 195 | polkit_object_path = "/org/gardesk/Garcard/TestAgent" |
| 170 | 196 | locale = "C" |
| 197 | +backend_healthcheck_secs = "7" | |
| 171 | 198 | "#, |
| 172 | 199 | ) |
| 173 | 200 | .expect("parse file config"); |
@@ -183,6 +210,7 @@ locale = "C" | ||
| 183 | 210 | Some("/org/gardesk/Garcard/TestAgent") |
| 184 | 211 | ); |
| 185 | 212 | assert_eq!(parsed.locale.as_deref(), Some("C")); |
| 213 | + assert_eq!(parsed.backend_healthcheck_secs.as_deref(), Some("7")); | |
| 186 | 214 | } |
| 187 | 215 | |
| 188 | 216 | #[test] |
@@ -206,4 +234,15 @@ locale = "C" | ||
| 206 | 234 | let err = AgentBackendMode::from_str("bad").expect_err("should fail"); |
| 207 | 235 | assert!(err.to_string().contains("unsupported backend mode")); |
| 208 | 236 | } |
| 237 | + | |
| 238 | + #[test] | |
| 239 | + fn parse_positive_secs_rejects_zero() { | |
| 240 | + let err = parse_positive_secs("0").expect_err("zero should fail"); | |
| 241 | + assert!(err.to_string().contains("greater than zero")); | |
| 242 | + } | |
| 243 | + | |
| 244 | + #[test] | |
| 245 | + fn parse_positive_secs_accepts_positive_integer() { | |
| 246 | + assert_eq!(parse_positive_secs("9").expect("valid"), 9); | |
| 247 | + } | |
| 209 | 248 | } |
garcard/src/daemon.rsmodified@@ -7,6 +7,7 @@ use serde_json::json; | ||
| 7 | 7 | use std::os::unix::fs::PermissionsExt; |
| 8 | 8 | use std::path::{Path, PathBuf}; |
| 9 | 9 | use std::sync::Arc; |
| 10 | +use std::time::Duration; | |
| 10 | 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; |
| 11 | 12 | use tokio::net::{UnixListener, UnixStream}; |
| 12 | 13 | use tokio::sync::mpsc; |
@@ -64,16 +65,37 @@ pub async fn run(config: Config) -> Result<()> { | ||
| 64 | 65 | .context("Failed to register SIGTERM handler")?; |
| 65 | 66 | let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) |
| 66 | 67 | .context("Failed to register SIGINT handler")?; |
| 68 | + let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) | |
| 69 | + .context("Failed to register SIGHUP handler")?; | |
| 70 | + let mut backend_maintenance = | |
| 71 | + tokio::time::interval(Duration::from_secs(config.backend_healthcheck_secs)); | |
| 72 | + backend_maintenance.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); | |
| 67 | 73 | |
| 68 | 74 | tracing::info!( |
| 69 | 75 | socket = %config.socket_path.display(), |
| 70 | 76 | pid = state.status().pid, |
| 71 | 77 | backend = backend.name(), |
| 78 | + backend_healthcheck_secs = config.backend_healthcheck_secs, | |
| 72 | 79 | "garcard daemon started" |
| 73 | 80 | ); |
| 74 | 81 | |
| 75 | 82 | loop { |
| 76 | 83 | tokio::select! { |
| 84 | + _ = backend_maintenance.tick() => { | |
| 85 | + if let Err(err) = backend.maintain() { | |
| 86 | + tracing::warn!( | |
| 87 | + error = %err, | |
| 88 | + backend = backend.name(), | |
| 89 | + "Backend maintenance check failed" | |
| 90 | + ); | |
| 91 | + } | |
| 92 | + } | |
| 93 | + _ = sighup.recv() => { | |
| 94 | + tracing::info!("Received SIGHUP; forcing backend reconnect"); | |
| 95 | + if let Err(err) = reconnect_backend(backend.as_mut()) { | |
| 96 | + tracing::warn!(error = %err, backend = backend.name(), "Forced backend reconnect failed"); | |
| 97 | + } | |
| 98 | + } | |
| 77 | 99 | _ = sigterm.recv() => { |
| 78 | 100 | tracing::info!("Received SIGTERM"); |
| 79 | 101 | break; |
@@ -114,6 +136,12 @@ pub async fn run(config: Config) -> Result<()> { | ||
| 114 | 136 | Ok(()) |
| 115 | 137 | } |
| 116 | 138 | |
| 139 | +fn reconnect_backend(backend: &mut dyn AuthAgentBackend) -> Result<()> { | |
| 140 | + backend.unregister()?; | |
| 141 | + backend.register()?; | |
| 142 | + Ok(()) | |
| 143 | +} | |
| 144 | + | |
| 117 | 145 | fn init_backend(config: &Config, auth_state: Arc<AuthState>) -> Result<Box<dyn AuthAgentBackend>> { |
| 118 | 146 | match config.agent_backend { |
| 119 | 147 | AgentBackendMode::Stub => { |
@@ -241,6 +269,9 @@ fn dispatch( | ||
| 241 | 269 | #[cfg(test)] |
| 242 | 270 | mod tests { |
| 243 | 271 | use super::*; |
| 272 | + use anyhow::Result as AnyResult; | |
| 273 | + use std::sync::Arc; | |
| 274 | + use std::sync::atomic::{AtomicUsize, Ordering}; | |
| 244 | 275 | |
| 245 | 276 | fn fake_state() -> RuntimeState { |
| 246 | 277 | RuntimeState::new("/tmp/garcard-test.sock".to_string(), "test-backend") |
@@ -276,10 +307,46 @@ mod tests { | ||
| 276 | 307 | agent_backend: AgentBackendMode::Auto, |
| 277 | 308 | polkit_object_path: "invalid path".to_string(), |
| 278 | 309 | locale: "C".to_string(), |
| 310 | + backend_healthcheck_secs: 5, | |
| 279 | 311 | }; |
| 280 | 312 | |
| 281 | 313 | let backend = init_backend(&config, Arc::new(AuthState::default())) |
| 282 | 314 | .expect("auto mode should fall back"); |
| 283 | 315 | assert_eq!(backend.name(), "stub-polkit-agent"); |
| 284 | 316 | } |
| 317 | + | |
| 318 | + struct TrackingBackend { | |
| 319 | + unregister_calls: Arc<AtomicUsize>, | |
| 320 | + register_calls: Arc<AtomicUsize>, | |
| 321 | + } | |
| 322 | + | |
| 323 | + impl AuthAgentBackend for TrackingBackend { | |
| 324 | + fn name(&self) -> &'static str { | |
| 325 | + "tracking-backend" | |
| 326 | + } | |
| 327 | + | |
| 328 | + fn register(&mut self) -> AnyResult<()> { | |
| 329 | + self.register_calls.fetch_add(1, Ordering::Relaxed); | |
| 330 | + Ok(()) | |
| 331 | + } | |
| 332 | + | |
| 333 | + fn unregister(&mut self) -> AnyResult<()> { | |
| 334 | + self.unregister_calls.fetch_add(1, Ordering::Relaxed); | |
| 335 | + Ok(()) | |
| 336 | + } | |
| 337 | + } | |
| 338 | + | |
| 339 | + #[test] | |
| 340 | + fn reconnect_backend_calls_unregister_then_register() { | |
| 341 | + let unregister_calls = Arc::new(AtomicUsize::new(0)); | |
| 342 | + let register_calls = Arc::new(AtomicUsize::new(0)); | |
| 343 | + let mut backend = TrackingBackend { | |
| 344 | + unregister_calls: Arc::clone(&unregister_calls), | |
| 345 | + register_calls: Arc::clone(®ister_calls), | |
| 346 | + }; | |
| 347 | + | |
| 348 | + reconnect_backend(&mut backend).expect("reconnect"); | |
| 349 | + assert_eq!(unregister_calls.load(Ordering::Relaxed), 1); | |
| 350 | + assert_eq!(register_calls.load(Ordering::Relaxed), 1); | |
| 351 | + } | |
| 285 | 352 | } |