Skip to main content

podbox/systemd/
units.rs

1//! `systemctl` command helpers: unit start/stop/restart, enable/failed
2//! queries, linger, daemon-reload, and guest-socket self-healing.
3//!
4//! Extracted verbatim from `systemd.rs`.
5
6use std::process::Command;
7
8use anyhow::{Context, Result};
9
10/// Whether systemctl is available on this system.
11pub fn is_available() -> bool {
12    which::which("systemctl").is_ok()
13}
14
15/// Ensure linger is enabled for the current user.
16pub fn enable_linger() -> Result<()> {
17    let whoami = std::env::var("USER").unwrap_or_default();
18    if whoami.is_empty() || which::which("loginctl").is_err() {
19        return Ok(());
20    }
21    let mut cmd = Command::new("loginctl");
22    cmd.args(["enable-linger", &whoami]);
23    let output = cmd
24        .stdout(std::process::Stdio::piped())
25        .stderr(std::process::Stdio::piped())
26        .spawn()
27        .context("failed to spawn loginctl")?
28        .wait_with_output()
29        .context("loginctl command failed")?;
30    if !output.status.success() {
31        let stderr = String::from_utf8_lossy(&output.stderr);
32        eprintln!("Warning: enable-linger failed: {stderr}");
33    } else {
34        println!("Linger enabled for user.");
35    }
36    Ok(())
37}
38
39/// Run `systemctl --user daemon-reload`.
40pub fn daemon_reload() -> Result<()> {
41    if !is_available() {
42        return Ok(());
43    }
44    let mut cmd = Command::new("systemctl");
45    cmd.args(["--user", "daemon-reload"]);
46    let output = cmd
47        .stdout(std::process::Stdio::piped())
48        .stderr(std::process::Stdio::piped())
49        .spawn()
50        .context("failed to spawn systemctl daemon-reload")?
51        .wait_with_output()
52        .context("systemctl daemon-reload failed")?;
53    if !output.status.success() {
54        let stderr = String::from_utf8_lossy(&output.stderr);
55        anyhow::bail!("daemon-reload failed: {}", stderr.trim());
56    }
57    Ok(())
58}
59
60/// Run `systemctl --user reset-failed` for a container's units.
61pub fn reset_failed(name: &str) -> Result<()> {
62    if !is_available() {
63        return Ok(());
64    }
65    let unit_names = [
66        format!("{name}.service"),
67        format!("{name}.socket"),
68        format!("{name}-host.service"),
69        format!("{name}-proxy.service"),
70        format!("{name}-compositor.service"),
71    ];
72    for unit in &unit_names {
73        let mut cmd = Command::new("systemctl");
74        cmd.args(["--user", "reset-failed", unit])
75            .stdout(std::process::Stdio::null())
76            .stderr(std::process::Stdio::null());
77        let _ = cmd.status();
78    }
79    Ok(())
80}
81
82/// Start and enable a socket unit.
83pub fn enable_now_socket(name: &str) -> Result<()> {
84    if !is_available() {
85        return Ok(());
86    }
87    let mut cmd = Command::new("systemctl");
88    cmd.args(["--user", "enable", "--now", &format!("{name}.socket")]);
89    let status = cmd
90        .status()
91        .context("failed to spawn systemctl enable --now")?;
92    if !status.success() {
93        anyhow::bail!("systemctl --user enable --now {name}.socket failed");
94    }
95    Ok(())
96}
97
98/// Stop socket and host service units.
99pub fn stop_socket_and_host(name: &str) -> Result<()> {
100    if !is_available() {
101        return Ok(());
102    }
103    for unit in [format!("{name}.socket"), format!("{name}-host.service")] {
104        let mut cmd = Command::new("systemctl");
105        cmd.args(["--user", "stop", &unit]);
106        let _ = cmd.status();
107    }
108    Ok(())
109}
110
111/// Stop the Wayland compositor proxy service if it exists.
112pub fn stop_compositor(name: &str) -> Result<()> {
113    if !is_available() {
114        return Ok(());
115    }
116    let mut cmd = Command::new("systemctl");
117    cmd.args(["--user", "stop", &format!("{name}-compositor.service")]);
118    let _ = cmd.status();
119    Ok(())
120}
121
122/// Path of the guest-facing socket for a container (`%t/podbox/<name>.sock`).
123pub(crate) fn guest_socket_path(name: &str) -> std::path::PathBuf {
124    let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
125        let uid = nix::unistd::getuid().as_raw();
126        format!("/run/user/{uid}")
127    });
128    std::path::PathBuf::from(runtime)
129        .join("podbox")
130        .join(format!("{name}.sock"))
131}
132
133/// Restart the container's socket unit so systemd rebinds a fresh socket file.
134///
135/// The `.socket` unit can outlive its filesystem entry: an external unlink or
136/// a RuntimeDirectory recreation leaves the unit "active (listening)" on an
137/// orphaned fd while the path is gone. A container bind-mounting that path
138/// then fails at create time with `statfs ...: no such file or directory`.
139fn rebind_guest_socket(name: &str) -> Result<()> {
140    let mut cmd = Command::new("systemctl");
141    cmd.args(["--user", "restart", &format!("{name}.socket")]);
142    let status = cmd.status().context("failed to spawn systemctl restart")?;
143    if !status.success() {
144        anyhow::bail!("systemctl --user restart {name}.socket failed");
145    }
146    Ok(())
147}
148
149/// Rebind the guest socket if its filesystem entry went missing.
150///
151/// Returns `true` when a heal was performed (socket was missing and the
152/// restart succeeded).
153pub(crate) fn heal_missing_guest_socket(name: &str) -> Result<bool> {
154    if guest_socket_path(name).exists() {
155        return Ok(false);
156    }
157    eprintln!(
158        "Warning: {} is missing but {}.socket is active — restarting the socket unit to rebind it.",
159        guest_socket_path(name).display(),
160        name
161    );
162    rebind_guest_socket(name)?;
163    Ok(true)
164}
165
166/// Start a service unit via `systemctl --user start`.
167pub fn start_unit(name: &str) -> Result<()> {
168    let mut cmd = Command::new("systemctl");
169    cmd.args(["--user", "start", &format!("{name}.service")]);
170    let status = cmd.status().context("failed to spawn systemctl start")?;
171    if !status.success() {
172        anyhow::bail!("systemctl start failed for '{name}.service'");
173    }
174    Ok(())
175}
176
177/// Stop a service unit via `systemctl --user stop`.
178pub fn stop_unit(name: &str) -> Result<()> {
179    let mut cmd = Command::new("systemctl");
180    cmd.args(["--user", "stop", &format!("{name}.service")]);
181    cmd.status()?;
182    Ok(())
183}
184
185/// Restart a service unit via `systemctl --user restart`.
186pub fn restart_unit(name: &str) -> Result<()> {
187    let mut cmd = Command::new("systemctl");
188    cmd.args(["--user", "restart", &format!("{name}.service")]);
189    cmd.status()?;
190    Ok(())
191}
192
193/// Check whether a unit is enabled in systemd.
194pub fn is_unit_enabled(name: &str) -> bool {
195    if !is_available() {
196        return false;
197    }
198    Command::new("systemctl")
199        .args([
200            "--user",
201            "--quiet",
202            "is-enabled",
203            &format!("{name}.service"),
204        ])
205        .status()
206        .map(|s| s.success())
207        .unwrap_or(false)
208}
209
210/// Check whether a unit is in the failed state.
211pub fn is_unit_failed(name: &str) -> bool {
212    if !is_available() {
213        return false;
214    }
215    Command::new("systemctl")
216        .args(["--user", "is-failed", &format!("{name}.service")])
217        .output()
218        .ok()
219        .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "failed")
220        .unwrap_or(false)
221}