Skip to main content

podbox/
systemd.rs

1//! systemd integration for podbox: unit control, status queries, and
2//! diagnostics. `systemctl` helpers live in [`units`], status parsing and
3//! diagnostics in [`status`].
4
5mod status;
6pub mod units;
7pub use status::UnitStatus;
8pub use units::{
9    daemon_reload, enable_linger, enable_now_socket, is_available, is_unit_enabled, is_unit_failed,
10    reset_failed, restart_unit, start_unit, stop_compositor, stop_socket_and_host, stop_unit,
11};
12
13use std::time::{Duration, Instant};
14
15use anyhow::Result;
16
17use crate::podman::{ContainerState, query_state};
18
19use status::{diagnostic_card, journal_tail, query_unit_status};
20use units::heal_missing_guest_socket;
21
22const POLL_INTERVAL_MS: u64 = 300;
23
24/// Start a container with friendly diagnostics on failure.
25///
26/// Checks for `NeedDaemonReload` and auto-fixes it. If the start fails,
27/// queries systemd and journalctl to build a diagnostic card for the user.
28pub fn start_unit_friendly(name: &str, timeout_secs: u64) -> Result<()> {
29    if !is_available() {
30        anyhow::bail!("systemctl not available");
31    }
32
33    // Check if daemon-reload is needed first
34    match query_unit_status(name) {
35        Ok(status) if status.need_daemon_reload => {
36            tracing::info!("systemd needs reload — running daemon-reload...");
37            daemon_reload()?;
38        }
39        Ok(_) => {}
40        Err(_) => {
41            // Unit might not exist yet — that's fine, we're about to try starting.
42        }
43    }
44
45    // Clear any previous failure so a unit that landed in `failed` (e.g. from
46    // an idle stop or a transient error) can be started again without the
47    // user having to run `systemctl --user reset-failed` manually.
48    reset_failed(name)?;
49
50    // Self-heal: if the guest socket file vanished while its unit stayed
51    // active, rebind it before starting — otherwise podman fails with
52    // `statfs .../podbox/<name>.sock: no such file or directory`.
53    let _ = heal_missing_guest_socket(name);
54
55    let attempt = || -> Result<()> {
56        start_unit(name)?;
57        wait_for_running(name, timeout_secs)
58    };
59
60    let mut start_result = attempt();
61
62    if start_result.is_err() {
63        // One retry: a socket that went missing mid-start gets rebound first.
64        if let Ok(true) = heal_missing_guest_socket(name) {
65            eprintln!("Retrying start after socket rebind...");
66            reset_failed(name)?;
67            start_result = attempt();
68        }
69    }
70
71    match start_result {
72        Ok(()) => Ok(()),
73        Err(_) => {
74            // Gather diagnostics
75            let status = query_unit_status(name).unwrap_or_default();
76            let journal = journal_tail(name, 10).ok();
77            let card = diagnostic_card(name, &status, journal.as_deref());
78            eprintln!("{card}");
79            anyhow::bail!("container '{name}' failed to start");
80        }
81    }
82}
83
84/// Poll until the container reaches Running state or timeout.
85fn wait_for_running(name: &str, timeout_secs: u64) -> Result<()> {
86    let deadline = Instant::now() + Duration::from_secs(timeout_secs);
87    loop {
88        match query_state(name)? {
89            ContainerState::Running => return Ok(()),
90            _ if Instant::now() >= deadline => {
91                let state = query_state(name)?;
92                anyhow::bail!(
93                    "container '{name}' did not become ready within {timeout_secs}s (final state: {state:?})",
94                );
95            }
96            _ => {
97                std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
98            }
99        }
100    }
101}