service_manager/
systemd.rs

1use crate::utils::wrap_output;
2
3use super::{
4    utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
5    ServiceStopCtx, ServiceUninstallCtx,
6};
7use std::{
8    fmt, io,
9    path::PathBuf,
10    process::{Command, Output, Stdio},
11};
12
13static SYSTEMCTL: &str = "systemctl";
14const SERVICE_FILE_PERMISSIONS: u32 = 0o644;
15
16/// Configuration settings tied to systemd services
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct SystemdConfig {
19    pub install: SystemdInstallConfig,
20}
21
22/// Configuration settings tied to systemd services during installation
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct SystemdInstallConfig {
25    pub start_limit_interval_sec: Option<u32>,
26    pub start_limit_burst: Option<u32>,
27    /// Systemd-specific restart policy. If `Some`, this takes precedence over the generic
28    /// `RestartPolicy` in `ServiceInstallCtx`. If `None`, the generic policy is used.
29    pub restart: Option<SystemdServiceRestartType>,
30    pub restart_sec: Option<u32>,
31}
32
33impl Default for SystemdInstallConfig {
34    fn default() -> Self {
35        Self {
36            start_limit_interval_sec: None,
37            start_limit_burst: None,
38            restart: None,
39            restart_sec: None,
40        }
41    }
42}
43
44#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
45pub enum SystemdServiceRestartType {
46    No,
47    Always,
48    OnSuccess,
49    OnFailure,
50    OnAbnormal,
51    OnAbort,
52    OnWatch,
53}
54
55impl Default for SystemdServiceRestartType {
56    fn default() -> Self {
57        Self::No
58    }
59}
60
61impl fmt::Display for SystemdServiceRestartType {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        match self {
64            Self::No => write!(f, "no"),
65            Self::Always => write!(f, "always"),
66            Self::OnSuccess => write!(f, "on-success"),
67            Self::OnFailure => write!(f, "on-failure"),
68            Self::OnAbnormal => write!(f, "on-abnormal"),
69            Self::OnAbort => write!(f, "on-abort"),
70            Self::OnWatch => write!(f, "on-watch"),
71        }
72    }
73}
74
75/// Implementation of [`ServiceManager`] for Linux's [systemd](https://en.wikipedia.org/wiki/Systemd)
76#[derive(Clone, Debug, Default, PartialEq, Eq)]
77pub struct SystemdServiceManager {
78    /// Whether or not this manager is operating at the user-level
79    pub user: bool,
80
81    /// Configuration settings tied to systemd services
82    pub config: SystemdConfig,
83}
84
85impl SystemdServiceManager {
86    /// Creates a new manager instance working with system services
87    pub fn system() -> Self {
88        Self::default()
89    }
90
91    /// Creates a new manager instance working with user services
92    pub fn user() -> Self {
93        Self::default().into_user()
94    }
95
96    /// Change manager to work with system services
97    pub fn into_system(self) -> Self {
98        Self {
99            config: self.config,
100            user: false,
101        }
102    }
103
104    /// Change manager to work with user services
105    pub fn into_user(self) -> Self {
106        Self {
107            config: self.config,
108            user: true,
109        }
110    }
111
112    /// Update manager to use the specified config
113    pub fn with_config(self, config: SystemdConfig) -> Self {
114        Self {
115            config,
116            user: self.user,
117        }
118    }
119}
120
121impl ServiceManager for SystemdServiceManager {
122    fn available(&self) -> io::Result<bool> {
123        match which::which(SYSTEMCTL) {
124            Ok(_) => Ok(true),
125            Err(which::Error::CannotFindBinaryPath) => Ok(false),
126            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
127        }
128    }
129
130    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
131        let dir_path = if self.user {
132            systemd_user_dir_path()?
133        } else {
134            systemd_global_dir_path()
135        };
136
137        std::fs::create_dir_all(&dir_path)?;
138
139        let script_name = ctx.label.to_script_name();
140        let script_path = dir_path.join(format!("{script_name}.service"));
141        let service = match ctx.contents {
142            Some(contents) => contents,
143            _ => make_service(
144                &self.config.install,
145                &script_name,
146                &ctx,
147                self.user,
148                ctx.autostart,
149            ),
150        };
151
152        utils::write_file(
153            script_path.as_path(),
154            service.as_bytes(),
155            SERVICE_FILE_PERMISSIONS,
156        )?;
157
158        if ctx.autostart {
159            wrap_output(systemctl(
160                "enable",
161                script_path.to_string_lossy().as_ref(),
162                self.user,
163            )?)?;
164        }
165
166        Ok(())
167    }
168
169    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
170        let dir_path = if self.user {
171            systemd_user_dir_path()?
172        } else {
173            systemd_global_dir_path()
174        };
175        let script_name = ctx.label.to_script_name();
176        let script_path = dir_path.join(format!("{script_name}.service"));
177
178        wrap_output(systemctl(
179            "disable",
180            script_path.to_string_lossy().as_ref(),
181            self.user,
182        )?)?;
183        std::fs::remove_file(script_path)
184    }
185
186    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
187        wrap_output(systemctl("start", &ctx.label.to_script_name(), self.user)?)?;
188        Ok(())
189    }
190
191    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
192        wrap_output(systemctl("stop", &ctx.label.to_script_name(), self.user)?)?;
193        Ok(())
194    }
195
196    fn level(&self) -> ServiceLevel {
197        if self.user {
198            ServiceLevel::User
199        } else {
200            ServiceLevel::System
201        }
202    }
203
204    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
205        match level {
206            ServiceLevel::System => self.user = false,
207            ServiceLevel::User => self.user = true,
208        }
209
210        Ok(())
211    }
212
213    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
214        let output = systemctl("status", &ctx.label.to_script_name(), self.user)?;
215        // ref: https://www.freedesktop.org/software/systemd/man/latest/systemctl.html#Exit%20status
216        match output.status.code() {
217            Some(4) => Ok(crate::ServiceStatus::NotInstalled),
218            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
219            Some(0) => Ok(crate::ServiceStatus::Running),
220            _ => Err(io::Error::new(
221                io::ErrorKind::Other,
222                format!(
223                    "Command failed with exit code {}: {}",
224                    output.status.code().unwrap_or(-1),
225                    String::from_utf8_lossy(&output.stderr)
226                ),
227            )),
228        }
229    }
230}
231
232fn systemctl(cmd: &str, label: &str, user: bool) -> io::Result<Output> {
233    let mut command = Command::new(SYSTEMCTL);
234
235    command
236        .stdin(Stdio::null())
237        .stdout(Stdio::piped())
238        .stderr(Stdio::piped());
239
240    if user {
241        command.arg("--user");
242    }
243
244    command.arg(cmd).arg(label).output()
245}
246
247#[inline]
248pub fn systemd_global_dir_path() -> PathBuf {
249    PathBuf::from("/etc/systemd/system")
250}
251
252pub fn systemd_user_dir_path() -> io::Result<PathBuf> {
253    Ok(dirs::config_dir()
254        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Unable to locate home directory"))?
255        .join("systemd")
256        .join("user"))
257}
258
259fn make_service(
260    config: &SystemdInstallConfig,
261    description: &str,
262    ctx: &ServiceInstallCtx,
263    user: bool,
264    autostart: bool,
265) -> String {
266    use std::fmt::Write as _;
267    let SystemdInstallConfig {
268        start_limit_interval_sec,
269        start_limit_burst,
270        restart: specific_restart_policy,
271        restart_sec: specific_restart_sec,
272    } = config;
273
274    let mut service = String::new();
275    let _ = writeln!(service, "[Unit]");
276    let _ = writeln!(service, "Description={description}");
277
278    if let Some(x) = start_limit_interval_sec {
279        let _ = writeln!(service, "StartLimitIntervalSec={x}");
280    }
281
282    if let Some(x) = start_limit_burst {
283        let _ = writeln!(service, "StartLimitBurst={x}");
284    }
285
286    let _ = writeln!(service, "[Service]");
287    if let Some(working_directory) = &ctx.working_directory {
288        let _ = writeln!(
289            service,
290            "WorkingDirectory={}",
291            working_directory.to_string_lossy()
292        );
293    }
294
295    if let Some(env_vars) = &ctx.environment {
296        for (var, val) in env_vars {
297            let _ = writeln!(service, "Environment=\"{var}={val}\"");
298        }
299    }
300
301    let program = ctx.program.to_string_lossy();
302    let args = ctx
303        .args
304        .clone()
305        .into_iter()
306        .map(|a| a.to_string_lossy().to_string())
307        .collect::<Vec<String>>()
308        .join(" ");
309    let _ = writeln!(service, "ExecStart={program} {args}");
310
311    // Handle restart configuration.
312    // Specific policy takes precedence over generic.
313    if let Some(restart_type) = specific_restart_policy {
314        if *restart_type != SystemdServiceRestartType::No {
315            let _ = writeln!(service, "Restart={restart_type}");
316        }
317        if let Some(delay) = specific_restart_sec {
318            let _ = writeln!(service, "RestartSec={delay}");
319        }
320    } else {
321        match ctx.restart_policy {
322            RestartPolicy::Never => {
323                // Don't write Restart= or RestartSec=
324            }
325            RestartPolicy::Always { delay_secs } => {
326                let _ = writeln!(service, "Restart=always");
327                if let Some(delay) = delay_secs {
328                    let _ = writeln!(service, "RestartSec={delay}");
329                }
330            }
331            RestartPolicy::OnFailure { delay_secs } => {
332                let _ = writeln!(service, "Restart=on-failure");
333                if let Some(delay) = delay_secs {
334                    let _ = writeln!(service, "RestartSec={delay}");
335                }
336            }
337            RestartPolicy::OnSuccess { delay_secs } => {
338                let _ = writeln!(service, "Restart=on-success");
339                if let Some(delay) = delay_secs {
340                    let _ = writeln!(service, "RestartSec={delay}");
341                }
342            }
343        }
344    }
345
346    // For Systemd, a user-mode service definition should *not* specify the username, since it runs
347    // as the current user. The service will not start correctly if the definition specifies the
348    // username, even if it's the same as the current user. The option for specifying a user really
349    // only applies for a system-level service that doesn't run as root.
350    if !user {
351        if let Some(username) = &ctx.username {
352            let _ = writeln!(service, "User={username}");
353        }
354    }
355
356    if user && autostart {
357        let _ = writeln!(service, "[Install]");
358        let _ = writeln!(service, "WantedBy=default.target");
359    } else if autostart {
360        let _ = writeln!(service, "[Install]");
361        let _ = writeln!(service, "WantedBy=multi-user.target");
362    }
363
364    service.trim().to_string()
365}