Skip to main content

running_process_platform_internal/platform_linux/
autostart.rs

1//! Linux login autostart: a systemd user unit under
2//! `$XDG_CONFIG_HOME/systemd/user/`.
3//!
4//! Registering writes the unit and runs `systemctl --user enable`. If
5//! `systemctl` is missing or fails -- a non-systemd Linux, or a session with no
6//! sd-bus -- the unit is still written and its path still returned, with a
7//! warning. Half a registration the operator can finish by hand beats none at
8//! all, and the file on disk is the half that is hard to reproduce.
9
10use std::path::PathBuf;
11use std::process::Command;
12
13use crate::platform::autostart::{shell_quote_single, AutostartError, AutostartProgram};
14
15/// Render the unit text without touching the filesystem.
16pub fn render_registration(program: &AutostartProgram<'_>) -> String {
17    let binary = shell_quote_single(&program.program.to_string_lossy());
18    let description = program.description;
19    let start = program.start_argument;
20    let stop = program.stop_argument;
21    format!(
22        "[Unit]\n\
23         Description={description}\n\
24         After=default.target\n\
25         \n\
26         [Service]\n\
27         Type=simple\n\
28         ExecStart={binary} {start}\n\
29         ExecStop={binary} {stop}\n\
30         Restart=on-failure\n\
31         RestartSec=5\n\
32         \n\
33         [Install]\n\
34         WantedBy=default.target\n",
35    )
36}
37
38pub fn register(program: &AutostartProgram<'_>) -> Result<PathBuf, AutostartError> {
39    let path = unit_path(program)?;
40    if let Some(parent) = path.parent() {
41        std::fs::create_dir_all(parent)?;
42    }
43    std::fs::write(&path, render_registration(program))?;
44
45    let unit = unit_filename(program);
46    let _ = Command::new("systemctl")
47        .args(["--user", "daemon-reload"])
48        .status();
49    match Command::new("systemctl")
50        .args(["--user", "enable", &unit])
51        .status()
52    {
53        Ok(status) if status.success() => {}
54        Ok(status) => {
55            eprintln!("warning: systemctl --user enable {unit} returned non-zero ({status:?})");
56        }
57        Err(error) => {
58            eprintln!("warning: systemctl --user enable {unit} failed to spawn: {error}");
59        }
60    }
61
62    Ok(path)
63}
64
65pub fn unregister(program: &AutostartProgram<'_>) -> Result<(), AutostartError> {
66    let path = unit_path(program)?;
67    let unit = unit_filename(program);
68    let _ = Command::new("systemctl")
69        .args(["--user", "disable", &unit])
70        .status();
71    if path.exists() {
72        std::fs::remove_file(&path)?;
73    }
74    let _ = Command::new("systemctl")
75        .args(["--user", "daemon-reload"])
76        .status();
77    Ok(())
78}
79
80fn unit_filename(program: &AutostartProgram<'_>) -> String {
81    format!("{}.service", program.identifier)
82}
83
84/// `$XDG_CONFIG_HOME/systemd/user/<identifier>.service`, falling back to
85/// `~/.config/` when `XDG_CONFIG_HOME` is unset.
86fn unit_path(program: &AutostartProgram<'_>) -> Result<PathBuf, AutostartError> {
87    let base = match std::env::var_os("XDG_CONFIG_HOME") {
88        Some(value) if !value.is_empty() => PathBuf::from(value),
89        _ => {
90            let home = std::env::var_os("HOME").ok_or_else(|| {
91                AutostartError::Resolve("neither XDG_CONFIG_HOME nor HOME is set".into())
92            })?;
93            PathBuf::from(home).join(".config")
94        }
95    };
96    Ok(base
97        .join("systemd")
98        .join("user")
99        .join(unit_filename(program)))
100}