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#[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: 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
77pub struct SystemdServiceManager {
78 pub user: bool,
80
81 pub config: SystemdConfig,
83}
84
85impl SystemdServiceManager {
86 pub fn system() -> Self {
88 Self::default()
89 }
90
91 pub fn user() -> Self {
93 Self::default().into_user()
94 }
95
96 pub fn into_system(self) -> Self {
98 Self {
99 config: self.config,
100 user: false,
101 }
102 }
103
104 pub fn into_user(self) -> Self {
106 Self {
107 config: self.config,
108 user: true,
109 }
110 }
111
112 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 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 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 }
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 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}