Skip to main content

running_process/boot_autostart/
linux.rs

1//! Linux backend: systemd user unit at
2//! `$XDG_CONFIG_HOME/systemd/user/runpm-daemon.service`.
3//!
4//! `install` writes the unit and runs `systemctl --user enable
5//! runpm-daemon.service`. If `systemctl` is missing or fails (operator
6//! is on a non-systemd Linux, or sd-bus is unavailable in this session)
7//! the file is still written and the path is returned, with a warning.
8
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12use super::{shell_quote_single, BootAutostartError, UnitPath};
13
14/// Canonical filename. Must match `uninstall`.
15const UNIT_FILENAME: &str = "runpm-daemon.service";
16
17/// Render the systemd unit file body with `daemon_binary` baked in.
18pub fn render_unit(daemon_binary: &Path) -> String {
19    let bin = shell_quote_single(&daemon_binary.to_string_lossy());
20    format!(
21        "[Unit]\n\
22         Description=runpm process supervisor (running-process daemon)\n\
23         After=default.target\n\
24         \n\
25         [Service]\n\
26         Type=simple\n\
27         ExecStart={bin} start\n\
28         ExecStop={bin} stop\n\
29         Restart=on-failure\n\
30         RestartSec=5\n\
31         \n\
32         [Install]\n\
33         WantedBy=default.target\n",
34    )
35}
36
37/// Resolve the unit-file path: `$XDG_CONFIG_HOME/systemd/user/runpm-daemon.service`
38/// with a `~/.config/systemd/user/` fallback when `XDG_CONFIG_HOME` is unset.
39pub fn unit_path() -> Result<PathBuf, BootAutostartError> {
40    let base = match std::env::var_os("XDG_CONFIG_HOME") {
41        Some(v) if !v.is_empty() => PathBuf::from(v),
42        _ => {
43            let home = std::env::var_os("HOME").ok_or_else(|| {
44                BootAutostartError::Resolve("neither XDG_CONFIG_HOME nor HOME is set".into())
45            })?;
46            PathBuf::from(home).join(".config")
47        }
48    };
49    Ok(base.join("systemd").join("user").join(UNIT_FILENAME))
50}
51
52pub fn install(daemon_binary: &Path) -> Result<UnitPath, BootAutostartError> {
53    let path = unit_path()?;
54    if let Some(parent) = path.parent() {
55        std::fs::create_dir_all(parent)?;
56    }
57    std::fs::write(&path, render_unit(daemon_binary))?;
58
59    // Best-effort daemon-reload + enable. A non-zero status means the
60    // file is written but the unit is not armed; the operator can run
61    // `systemctl --user enable` later. We surface the failure as a
62    // tracing warning rather than an error so the caller still gets the
63    // path that was written.
64    let _ = Command::new("systemctl")
65        .args(["--user", "daemon-reload"])
66        .status();
67    match Command::new("systemctl")
68        .args(["--user", "enable", UNIT_FILENAME])
69        .status()
70    {
71        Ok(s) if s.success() => {}
72        Ok(s) => {
73            eprintln!("warning: systemctl --user enable {UNIT_FILENAME} returned non-zero ({s:?})");
74        }
75        Err(e) => {
76            eprintln!("warning: systemctl --user enable {UNIT_FILENAME} failed to spawn: {e}");
77        }
78    }
79
80    Ok(UnitPath(path))
81}
82
83pub fn uninstall() -> Result<(), BootAutostartError> {
84    let path = unit_path()?;
85    let _ = Command::new("systemctl")
86        .args(["--user", "disable", UNIT_FILENAME])
87        .status();
88    if path.exists() {
89        std::fs::remove_file(&path)?;
90    }
91    let _ = Command::new("systemctl")
92        .args(["--user", "daemon-reload"])
93        .status();
94    Ok(())
95}