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#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct SystemdConfig {
19 pub install: SystemdInstallConfig,
20}
21
22#[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#[derive(Clone, Debug, Default, PartialEq, Eq)]
75pub struct SystemdServiceManager {
76 pub user: bool,
78
79 pub config: SystemdConfig,
81}
82
83impl SystemdServiceManager {
84 pub fn system() -> Self {
86 Self::default()
87 }
88
89 pub fn user() -> Self {
91 Self::default().into_user()
92 }
93
94 pub fn into_system(self) -> Self {
96 Self {
97 config: self.config,
98 user: false,
99 }
100 }
101
102 pub fn into_user(self) -> Self {
104 Self {
105 config: self.config,
106 user: true,
107 }
108 }
109
110 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 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 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}