Skip to main content

podbox/quadlet_install/
units.rs

1//! Custom systemd unit writing, `podman quadlet install` invocation
2//! (flat-file and `--application` variants), and leftover-unit removal.
3//!
4//! Extracted verbatim from `quadlet_install.rs`.
5
6use std::path::Path;
7
8use anyhow::Result;
9
10use crate::systemd;
11
12use super::paths::quadlet_dir;
13
14/// Write custom systemd units (socket, host-service, optional dbus-proxy
15/// and compositor) to sdir.
16fn write_custom_units(
17    name: &str,
18    sdir: &Path,
19    socket_content: &str,
20    host_service_content: &str,
21    dbus_proxy_content: Option<&str>,
22    compositor_service_content: Option<&str>,
23) -> Result<()> {
24    std::fs::create_dir_all(sdir)?;
25    std::fs::write(sdir.join(format!("{name}.socket")), socket_content)?;
26    std::fs::write(
27        sdir.join(format!("{name}-host.service")),
28        host_service_content,
29    )?;
30    if let Some(proxy) = dbus_proxy_content {
31        std::fs::write(sdir.join(format!("{name}-proxy.service")), proxy)?;
32    }
33    if let Some(comp) = compositor_service_content {
34        std::fs::write(sdir.join(format!("{name}-compositor.service")), comp)?;
35    }
36    write_clean_stop_dropin(name, sdir)?;
37    Ok(())
38}
39
40/// Container service units are generated by Quadlet, which cannot express
41/// `SuccessExitStatus`. When the guest's idle timer fires, the host stops the
42/// unit via `systemctl stop`; systemd SIGTERMs the container, the main process
43/// exits 143, and the unit would otherwise land in `failed`. A drop-in marks
44/// SIGTERM (and a graceful 0 exit) as a clean stop so idle shutdown settles in
45/// `inactive` instead of `failed`.
46fn write_clean_stop_dropin(name: &str, sdir: &Path) -> Result<()> {
47    let dir = sdir.join(format!("{name}.service.d"));
48    std::fs::create_dir_all(&dir)?;
49    std::fs::write(
50        dir.join("99-podbox-clean-stop.conf"),
51        "[Service]\nSuccessExitStatus=0 143 SIGTERM SIGINT\n",
52    )?;
53    Ok(())
54}
55
56/// Activate custom units after Quadlet files are in place.
57pub(crate) fn finalize_units(
58    name: &str,
59    sdir: &Path,
60    socket_content: &str,
61    host_service_content: &str,
62    dbus_proxy_content: Option<&str>,
63    compositor_service_content: Option<&str>,
64    use_wayland_proxy: bool,
65) -> Result<()> {
66    write_custom_units(
67        name,
68        sdir,
69        socket_content,
70        host_service_content,
71        dbus_proxy_content,
72        compositor_service_content,
73    )?;
74    println!("Systemd units installed to {}", sdir.display());
75
76    systemd::daemon_reload()?;
77    systemd::reset_failed(name)?;
78    systemd::stop_socket_and_host(name)?;
79    if use_wayland_proxy {
80        systemd::stop_compositor(name)?;
81    }
82    systemd::enable_now_socket(name)?;
83    Ok(())
84}
85
86/// Install `.container` (+ optional `.build`) via `podman quadlet install` using
87/// **file** arguments, not a directory.
88///
89/// Keeps the flat layout (`…/systemd/<name>.container`) for Podman 5.6–5.x.
90pub(crate) fn podman_quadlet_install_files(
91    name: &str,
92    container_content: &str,
93    build_content: Option<&str>,
94) -> Result<()> {
95    let tmp = std::env::temp_dir().join(format!("podbox-install-{name}"));
96    let _ = std::fs::remove_dir_all(&tmp);
97    std::fs::create_dir_all(&tmp)?;
98
99    let container_path = tmp.join(format!("{name}.container"));
100    std::fs::write(&container_path, container_content)?;
101
102    let mut args: Vec<std::ffi::OsString> = vec![
103        "quadlet".into(),
104        "install".into(),
105        "--replace".into(),
106        container_path.into(),
107    ];
108
109    if let Some(bc) = build_content {
110        let build_path = tmp.join(format!("{name}.build"));
111        std::fs::write(&build_path, bc)?;
112        args.push(build_path.into());
113    }
114
115    let output = crate::process::run_piped("podman", &args)?;
116    let _ = std::fs::remove_dir_all(&tmp);
117    if !output.status.success() {
118        let stderr = String::from_utf8_lossy(&output.stderr);
119        anyhow::bail!("podman quadlet install failed: {stderr}");
120    }
121    println!("Quadlet files installed via podman quadlet install.");
122    Ok(())
123}
124
125/// Install `.container` (+ optional `.build`) via `podman quadlet install` using
126/// **directory** arguments with `--application` for Podman 6.x.
127///
128/// Podman 6 requires `--application` when the source is a directory. The units
129/// end up at `…/systemd/<name>/<name>.container`.
130pub(crate) fn podman_quadlet_install_application(
131    name: &str,
132    container_content: &str,
133    build_content: Option<&str>,
134) -> Result<()> {
135    let tmp = std::env::temp_dir().join(format!("podbox-install-{name}"));
136    let _ = std::fs::remove_dir_all(&tmp);
137    std::fs::create_dir_all(&tmp)?;
138
139    std::fs::write(tmp.join(format!("{name}.container")), container_content)?;
140    if let Some(bc) = build_content {
141        std::fs::write(tmp.join(format!("{name}.build")), bc)?;
142    }
143
144    let args: Vec<std::ffi::OsString> = vec![
145        "quadlet".into(),
146        "install".into(),
147        "--replace".into(),
148        "--application".into(),
149        name.into(),
150        tmp.clone().into(),
151    ];
152
153    let output = crate::process::run_piped("podman", &args)?;
154    let _ = std::fs::remove_dir_all(&tmp);
155    if !output.status.success() {
156        let stderr = String::from_utf8_lossy(&output.stderr);
157        anyhow::bail!("podman quadlet install --application failed: {stderr}");
158    }
159    println!("Quadlet files installed via podman quadlet install --application {name}.");
160    Ok(())
161}
162
163/// Best-effort removal of leftover flat unit files (`.container`, `.build`).
164///
165/// Called before a `--application` install to avoid dual installs from old
166/// flat layouts.
167pub(crate) fn remove_flat_units(name: &str) {
168    let qdir = quadlet_dir();
169    for ext in ["container", "build"] {
170        let path = qdir.join(format!("{name}.{ext}"));
171        if !path.exists() {
172            continue;
173        }
174        // Best-effort podman quadlet rm first, then manual delete as fallback.
175        let args: Vec<std::ffi::OsString> = vec![
176            "quadlet".into(),
177            "rm".into(),
178            format!("{name}.{ext}").into(),
179        ];
180        let _ = crate::process::run_piped("podman", &args);
181        // Manual fallback in case podman rm failed.
182        let _ = std::fs::remove_file(&path);
183    }
184}
185
186/// Best-effort removal of leftover application-scoped install dirs.
187pub(crate) fn remove_application_dir(name: &str) {
188    let app_dir = quadlet_dir().join(name);
189    if app_dir.is_dir() {
190        let _ = std::fs::remove_dir_all(&app_dir);
191    }
192}