gardesk/garcard / 37b841d

Browse files

close sprint two blockers

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
37b841dc7b138df51b29d45d034f2396f6e2caa9
Parents
617b876
Tree
eb65dc2

8 changed files

StatusFile+-
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
 7. `GARCARD_POLKIT_HELPER_SOCKET`
25
 7. `GARCARD_POLKIT_HELPER_SOCKET`
26
 8. `GARCARD_PROMPT_COMMAND`
26
 8. `GARCARD_PROMPT_COMMAND`
27
 9. `GARCARD_PROMPT_TIMEOUT_SECS`
27
 9. `GARCARD_PROMPT_TIMEOUT_SECS`
28
+10. `GARCARD_BACKEND_HEALTHCHECK_SECS`
28
 
29
 
29
 See `examples/config.toml` for a starter file.
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
 # Locale sent to polkit authority registration.
21
 # Locale sent to polkit authority registration.
22
 locale = "en_US.UTF-8"
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
 1. One active request at a time.
58
 1. One active request at a time.
59
 2. Additional requests queue and process FIFO.
59
 2. Additional requests queue and process FIFO.
60
 3. No deadlock after cancel/failure/success.
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
 ACTION_ID="${1:-org.freedesktop.login1.power-off}"
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
 if ! command -v pkcheck >/dev/null 2>&1; then
6
 if ! command -v pkcheck >/dev/null 2>&1; then
13
   echo "pkcheck not found; install polkit tools to run live auth validation"
7
   echo "pkcheck not found; install polkit tools to run live auth validation"
14
   exit 1
8
   exit 1
15
 fi
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
 echo "[1/5] Check daemon connectivity"
21
 echo "[1/5] Check daemon connectivity"
18
-garcardctl ping
22
+run_garcardctl ping
19
 
23
 
20
 echo "[2/5] Check daemon status"
24
 echo "[2/5] Check daemon status"
21
-garcardctl status
25
+run_garcardctl status
22
 
26
 
23
 echo "[3/5] Check pre-auth summary"
27
 echo "[3/5] Check pre-auth summary"
24
-garcardctl auth-summary
28
+run_garcardctl auth-summary
25
 
29
 
26
 echo "[4/5] Trigger interactive policy check"
30
 echo "[4/5] Trigger interactive policy check"
27
 echo "Action ID: ${ACTION_ID}"
31
 echo "Action ID: ${ACTION_ID}"
@@ -33,7 +37,7 @@ set -e
33
 echo "pkcheck exit code: ${PKCHECK_RC}"
37
 echo "pkcheck exit code: ${PKCHECK_RC}"
34
 
38
 
35
 echo "[5/5] Check post-auth summary"
39
 echo "[5/5] Check post-auth summary"
36
-garcardctl auth-summary
40
+run_garcardctl auth-summary
37
 
41
 
38
 cat <<'EOF'
42
 cat <<'EOF'
39
 Exit code hints:
43
 Exit code hints:
garcard/src/agent.rsmodified
@@ -16,6 +16,9 @@ pub trait AuthAgentBackend {
16
     fn name(&self) -> &'static str;
16
     fn name(&self) -> &'static str;
17
     fn register(&mut self) -> Result<()>;
17
     fn register(&mut self) -> Result<()>;
18
     fn unregister(&mut self) -> Result<()>;
18
     fn unregister(&mut self) -> Result<()>;
19
+    fn maintain(&mut self) -> Result<()> {
20
+        Ok(())
21
+    }
19
 }
22
 }
20
 
23
 
21
 /// Placeholder backend used during Sprint 01 scaffolding.
24
 /// Placeholder backend used during Sprint 01 scaffolding.
@@ -472,6 +475,21 @@ impl PolkitAgent {
472
         )?;
475
         )?;
473
         Ok(proxy)
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
 impl AuthAgentBackend for PolkitAgent {
495
 impl AuthAgentBackend for PolkitAgent {
@@ -506,7 +524,7 @@ impl AuthAgentBackend for PolkitAgent {
506
                 &(
524
                 &(
507
                     &self.subject,
525
                     &self.subject,
508
                     self.locale.as_str(),
526
                     self.locale.as_str(),
509
-                    self.object_path.clone(),
527
+                    self.object_path.to_string(),
510
                 ),
528
                 ),
511
             )?;
529
             )?;
512
             Ok(())
530
             Ok(())
@@ -534,13 +552,10 @@ impl AuthAgentBackend for PolkitAgent {
534
         }
552
         }
535
 
553
 
536
         if let Some(connection) = &self.connection {
554
         if let Some(connection) = &self.connection {
537
-            match Self::proxy(connection)?.call::<_, _, ()>(
555
+            match Self::proxy(connection) {
556
+                Ok(proxy) => match proxy.call::<_, _, ()>(
538
                     "UnregisterAuthenticationAgent",
557
                     "UnregisterAuthenticationAgent",
539
-                &(
558
+                    &(&self.subject, self.object_path.to_string()),
540
-                    &self.subject,
541
-                    self.locale.as_str(),
542
-                    self.object_path.clone(),
543
-                ),
544
                 ) {
559
                 ) {
545
                     Ok(()) => {
560
                     Ok(()) => {
546
                         tracing::info!(
561
                         tracing::info!(
@@ -549,7 +564,17 @@ impl AuthAgentBackend for PolkitAgent {
549
                         );
564
                         );
550
                     }
565
                     }
551
                     Err(err) => {
566
                     Err(err) => {
552
-                    tracing::warn!(error = %err, "Failed to unregister polkit authentication agent");
567
+                        tracing::warn!(
568
+                            error = %err,
569
+                            "Failed to unregister polkit authentication agent"
570
+                        );
571
+                    }
572
+                },
573
+                Err(err) => {
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
         self.connection = None;
591
         self.connection = None;
567
         Ok(())
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
 fn build_subject() -> Subject {
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
     let mut details = HashMap::new();
628
     let mut details = HashMap::new();
573
     details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
629
     details.insert("pid".to_string(), OwnedValue::from(std::process::id()));
574
     details.insert(
630
     details.insert(
575
         "uid".to_string(),
631
         "uid".to_string(),
576
         OwnedValue::from(nix::unistd::geteuid().as_raw()),
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
     ("unix-process".to_string(), details)
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
 #[cfg(test)]
666
 #[cfg(test)]
582
 mod tests {
667
 mod tests {
583
     use super::*;
668
     use super::*;
@@ -608,9 +693,15 @@ mod tests {
608
     #[test]
693
     #[test]
609
     fn subject_uses_unix_process_kind() {
694
     fn subject_uses_unix_process_kind() {
610
         let subject = build_subject();
695
         let subject = build_subject();
611
-        assert_eq!(subject.0, "unix-process");
696
+        match subject.0.as_str() {
697
+            "unix-session" => assert!(subject.1.contains_key("session-id")),
698
+            "unix-process" => {
612
                 assert!(subject.1.contains_key("pid"));
699
                 assert!(subject.1.contains_key("pid"));
613
                 assert!(subject.1.contains_key("uid"));
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
     #[test]
707
     #[test]
garcard/src/config.rsmodified
@@ -12,6 +12,7 @@ pub struct Config {
12
     pub agent_backend: AgentBackendMode,
12
     pub agent_backend: AgentBackendMode,
13
     pub polkit_object_path: String,
13
     pub polkit_object_path: String,
14
     pub locale: String,
14
     pub locale: String,
15
+    pub backend_healthcheck_secs: u64,
15
 }
16
 }
16
 
17
 
17
 impl Default for Config {
18
 impl Default for Config {
@@ -22,6 +23,7 @@ impl Default for Config {
22
             agent_backend: AgentBackendMode::Auto,
23
             agent_backend: AgentBackendMode::Auto,
23
             polkit_object_path: "/org/gardesk/Garcard/AuthAgent".to_string(),
24
             polkit_object_path: "/org/gardesk/Garcard/AuthAgent".to_string(),
24
             locale: std::env::var("LANG").unwrap_or_else(|_| "en_US.UTF-8".to_string()),
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
         if let Some(raw_locale) = std::env::var_os("GARCARD_LOCALE") {
52
         if let Some(raw_locale) = std::env::var_os("GARCARD_LOCALE") {
51
             cfg.locale = raw_locale.to_string_lossy().to_string();
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
         Ok(cfg)
62
         Ok(cfg)
55
     }
63
     }
@@ -84,6 +92,12 @@ impl Config {
84
         if let Some(locale) = file_cfg.locale {
92
         if let Some(locale) = file_cfg.locale {
85
             self.locale = locale;
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
         Ok(())
102
         Ok(())
89
     }
103
     }
@@ -126,6 +140,7 @@ struct FileConfig {
126
     agent_backend: Option<String>,
140
     agent_backend: Option<String>,
127
     polkit_object_path: Option<String>,
141
     polkit_object_path: Option<String>,
128
     locale: Option<String>,
142
     locale: Option<String>,
143
+    backend_healthcheck_secs: Option<String>,
129
 }
144
 }
130
 
145
 
131
 fn config_path() -> Option<PathBuf> {
146
 fn config_path() -> Option<PathBuf> {
@@ -143,6 +158,17 @@ fn parse_octal_mode(raw: &str) -> Result<u32> {
143
     Ok(mode)
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
 #[cfg(test)]
172
 #[cfg(test)]
147
 mod tests {
173
 mod tests {
148
     use super::*;
174
     use super::*;
@@ -168,6 +194,7 @@ socket_mode = "640"
168
 agent_backend = "stub"
194
 agent_backend = "stub"
169
 polkit_object_path = "/org/gardesk/Garcard/TestAgent"
195
 polkit_object_path = "/org/gardesk/Garcard/TestAgent"
170
 locale = "C"
196
 locale = "C"
197
+backend_healthcheck_secs = "7"
171
 "#,
198
 "#,
172
         )
199
         )
173
         .expect("parse file config");
200
         .expect("parse file config");
@@ -183,6 +210,7 @@ locale = "C"
183
             Some("/org/gardesk/Garcard/TestAgent")
210
             Some("/org/gardesk/Garcard/TestAgent")
184
         );
211
         );
185
         assert_eq!(parsed.locale.as_deref(), Some("C"));
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
     #[test]
216
     #[test]
@@ -206,4 +234,15 @@ locale = "C"
206
         let err = AgentBackendMode::from_str("bad").expect_err("should fail");
234
         let err = AgentBackendMode::from_str("bad").expect_err("should fail");
207
         assert!(err.to_string().contains("unsupported backend mode"));
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
 use std::os::unix::fs::PermissionsExt;
7
 use std::os::unix::fs::PermissionsExt;
8
 use std::path::{Path, PathBuf};
8
 use std::path::{Path, PathBuf};
9
 use std::sync::Arc;
9
 use std::sync::Arc;
10
+use std::time::Duration;
10
 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
11
 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
11
 use tokio::net::{UnixListener, UnixStream};
12
 use tokio::net::{UnixListener, UnixStream};
12
 use tokio::sync::mpsc;
13
 use tokio::sync::mpsc;
@@ -64,16 +65,37 @@ pub async fn run(config: Config) -> Result<()> {
64
         .context("Failed to register SIGTERM handler")?;
65
         .context("Failed to register SIGTERM handler")?;
65
     let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
66
     let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
66
         .context("Failed to register SIGINT handler")?;
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
     tracing::info!(
74
     tracing::info!(
69
         socket = %config.socket_path.display(),
75
         socket = %config.socket_path.display(),
70
         pid = state.status().pid,
76
         pid = state.status().pid,
71
         backend = backend.name(),
77
         backend = backend.name(),
78
+        backend_healthcheck_secs = config.backend_healthcheck_secs,
72
         "garcard daemon started"
79
         "garcard daemon started"
73
     );
80
     );
74
 
81
 
75
     loop {
82
     loop {
76
         tokio::select! {
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
             _ = sigterm.recv() => {
99
             _ = sigterm.recv() => {
78
                 tracing::info!("Received SIGTERM");
100
                 tracing::info!("Received SIGTERM");
79
                 break;
101
                 break;
@@ -114,6 +136,12 @@ pub async fn run(config: Config) -> Result<()> {
114
     Ok(())
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
 fn init_backend(config: &Config, auth_state: Arc<AuthState>) -> Result<Box<dyn AuthAgentBackend>> {
145
 fn init_backend(config: &Config, auth_state: Arc<AuthState>) -> Result<Box<dyn AuthAgentBackend>> {
118
     match config.agent_backend {
146
     match config.agent_backend {
119
         AgentBackendMode::Stub => {
147
         AgentBackendMode::Stub => {
@@ -241,6 +269,9 @@ fn dispatch(
241
 #[cfg(test)]
269
 #[cfg(test)]
242
 mod tests {
270
 mod tests {
243
     use super::*;
271
     use super::*;
272
+    use anyhow::Result as AnyResult;
273
+    use std::sync::Arc;
274
+    use std::sync::atomic::{AtomicUsize, Ordering};
244
 
275
 
245
     fn fake_state() -> RuntimeState {
276
     fn fake_state() -> RuntimeState {
246
         RuntimeState::new("/tmp/garcard-test.sock".to_string(), "test-backend")
277
         RuntimeState::new("/tmp/garcard-test.sock".to_string(), "test-backend")
@@ -276,10 +307,46 @@ mod tests {
276
             agent_backend: AgentBackendMode::Auto,
307
             agent_backend: AgentBackendMode::Auto,
277
             polkit_object_path: "invalid path".to_string(),
308
             polkit_object_path: "invalid path".to_string(),
278
             locale: "C".to_string(),
309
             locale: "C".to_string(),
310
+            backend_healthcheck_secs: 5,
279
         };
311
         };
280
 
312
 
281
         let backend = init_backend(&config, Arc::new(AuthState::default()))
313
         let backend = init_backend(&config, Arc::new(AuthState::default()))
282
             .expect("auto mode should fall back");
314
             .expect("auto mode should fall back");
283
         assert_eq!(backend.name(), "stub-polkit-agent");
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(&register_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
 }