service_manager/
systemd.rs

1use crate::utils::wrap_output;
2
3use super::{
4    utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
5    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    pub restart: SystemdServiceRestartType,
28    pub restart_sec: Option<u32>,
29}
30
31impl Default for SystemdInstallConfig {
32    fn default() -> Self {
33        Self {
34            start_limit_interval_sec: None,
35            start_limit_burst: None,
36            restart: SystemdServiceRestartType::OnFailure,
37            restart_sec: None,
38        }
39    }
40}
41
42#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
43pub enum SystemdServiceRestartType {
44    No,
45    Always,
46    OnSuccess,
47    OnFailure,
48    OnAbnormal,
49    OnAbort,
50    OnWatch,
51}
52
53impl Default for SystemdServiceRestartType {
54    fn default() -> Self {
55        Self::No
56    }
57}
58
59impl fmt::Display for SystemdServiceRestartType {
60    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61        match self {
62            Self::No => write!(f, "no"),
63            Self::Always => write!(f, "always"),
64            Self::OnSuccess => write!(f, "on-success"),
65            Self::OnFailure => write!(f, "on-failure"),
66            Self::OnAbnormal => write!(f, "on-abnormal"),
67            Self::OnAbort => write!(f, "on-abort"),
68            Self::OnWatch => write!(f, "on-watch"),
69        }
70    }
71}
72
73/// Implementation of [`ServiceManager`] for Linux's [systemd](https://en.wikipedia.org/wiki/Systemd)
74#[derive(Clone, Debug, Default, PartialEq, Eq)]
75pub struct SystemdServiceManager {
76    /// Whether or not this manager is operating at the user-level
77    pub user: bool,
78
79    /// Configuration settings tied to systemd services
80    pub config: SystemdConfig,
81}
82
83impl SystemdServiceManager {
84    /// Creates a new manager instance working with system services
85    pub fn system() -> Self {
86        Self::default()
87    }
88
89    /// Creates a new manager instance working with user services
90    pub fn user() -> Self {
91        Self::default().into_user()
92    }
93
94    /// Change manager to work with system services
95    pub fn into_system(self) -> Self {
96        Self {
97            config: self.config,
98            user: false,
99        }
100    }
101
102    /// Change manager to work with user services
103    pub fn into_user(self) -> Self {
104        Self {
105            config: self.config,
106            user: true,
107        }
108    }
109
110    /// Update manager to use the specified config
111    pub fn with_config(self, config: SystemdConfig) -> Self {
112        Self {
113            config,
114            user: self.user,
115        }
116    }
117}
118
119impl ServiceManager for SystemdServiceManager {
120    fn available(&self) -> io::Result<bool> {
121        match which::which(SYSTEMCTL) {
122            Ok(_) => Ok(true),
123            Err(which::Error::CannotFindBinaryPath) => Ok(false),
124            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
125        }
126    }
127
128    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
129        let dir_path = if self.user {
130            systemd_user_dir_path()?
131        } else {
132            systemd_global_dir_path()
133        };
134
135        std::fs::create_dir_all(&dir_path)?;
136
137        let script_name = ctx.label.to_script_name();
138        let script_path = dir_path.join(format!("{script_name}.service"));
139        let service = match ctx.contents {
140            Some(contents) => contents,
141            _ => make_service(
142                &self.config.install,
143                &script_name,
144                &ctx,
145                self.user,
146                ctx.autostart,
147                ctx.disable_restart_on_failure
148            ),
149        };
150
151        utils::write_file(
152            script_path.as_path(),
153            service.as_bytes(),
154            SERVICE_FILE_PERMISSIONS,
155        )?;
156
157        if ctx.autostart {
158            wrap_output(systemctl(
159                "enable",
160                script_path.to_string_lossy().as_ref(),
161                self.user,
162            )?)?;
163        }
164
165        Ok(())
166    }
167
168    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
169        let dir_path = if self.user {
170            systemd_user_dir_path()?
171        } else {
172            systemd_global_dir_path()
173        };
174        let script_name = ctx.label.to_script_name();
175        let script_path = dir_path.join(format!("{script_name}.service"));
176
177        wrap_output(systemctl(
178            "disable",
179            script_path.to_string_lossy().as_ref(),
180            self.user,
181        )?)?;
182        std::fs::remove_file(script_path)
183    }
184
185    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
186        wrap_output(systemctl("start", &ctx.label.to_script_name(), self.user)?)?;
187        Ok(())
188    }
189
190    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
191        wrap_output(systemctl("stop", &ctx.label.to_script_name(), self.user)?)?;
192        Ok(())
193    }
194
195    fn level(&self) -> ServiceLevel {
196        if self.user {
197            ServiceLevel::User
198        } else {
199            ServiceLevel::System
200        }
201    }
202
203    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
204        match level {
205            ServiceLevel::System => self.user = false,
206            ServiceLevel::User => self.user = true,
207        }
208
209        Ok(())
210    }
211
212    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
213        let output = systemctl("status", &ctx.label.to_script_name(), self.user)?;
214        // ref: https://www.freedesktop.org/software/systemd/man/latest/systemctl.html#Exit%20status
215        match output.status.code() {
216            Some(4) => Ok(crate::ServiceStatus::NotInstalled),
217            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
218            Some(0) => Ok(crate::ServiceStatus::Running),
219            _ => Err(io::Error::new(
220                io::ErrorKind::Other,
221                format!(
222                    "Command failed with exit code {}: {}",
223                    output.status.code().unwrap_or(-1),
224                    String::from_utf8_lossy(&output.stderr)
225                ),
226            )),
227        }
228    }
229}
230
231fn systemctl(cmd: &str, label: &str, user: bool) -> io::Result<Output> {
232    let mut command = Command::new(SYSTEMCTL);
233
234    command
235        .stdin(Stdio::null())
236        .stdout(Stdio::piped())
237        .stderr(Stdio::piped());
238
239    if user {
240        command.arg("--user");
241    }
242
243    command.arg(cmd).arg(label).output()
244}
245
246#[inline]
247pub fn systemd_global_dir_path() -> PathBuf {
248    PathBuf::from("/etc/systemd/system")
249}
250
251pub fn systemd_user_dir_path() -> io::Result<PathBuf> {
252    Ok(dirs::config_dir()
253        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Unable to locate home directory"))?
254        .join("systemd")
255        .join("user"))
256}
257
258fn make_service(
259    config: &SystemdInstallConfig,
260    description: &str,
261    ctx: &ServiceInstallCtx,
262    user: bool,
263    autostart: bool,
264    disable_restart_on_failure: bool
265) -> String {
266    use std::fmt::Write as _;
267    let SystemdInstallConfig {
268        start_limit_interval_sec,
269        start_limit_burst,
270        restart,
271        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    if !disable_restart_on_failure {
312        if *restart != SystemdServiceRestartType::No {
313            let _ = writeln!(service, "Restart={restart}");
314        }
315
316        if let Some(x) = restart_sec {
317            let _ = writeln!(service, "RestartSec={x}");
318        }
319    }
320
321    // For Systemd, a user-mode service definition should *not* specify the username, since it runs
322    // as the current user. The service will not start correctly if the definition specifies the
323    // username, even if it's the same as the current user. The option for specifying a user really
324    // only applies for a system-level service that doesn't run as root.
325    if !user {
326        if let Some(username) = &ctx.username {
327            let _ = writeln!(service, "User={username}");
328        }
329    }
330
331    if user && autostart {
332        let _ = writeln!(service, "[Install]");
333        let _ = writeln!(service, "WantedBy=default.target");
334    } else if autostart {
335        let _ = writeln!(service, "[Install]");
336        let _ = writeln!(service, "WantedBy=multi-user.target");
337    }
338
339    service.trim().to_string()
340}