1use super::{list_services, Config, ServiceRef};
2pub use crate::systemd::generate_file;
3use crate::systemd::parse_systemd;
4use crate::{print_command, FsServiceDetails, ServiceDetails};
5use anyhow::{anyhow, bail, Context, Result};
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10pub(super) fn get_service_directories() -> Config {
11 let mut user_dirs = Vec::new();
12 let mut system_dirs = Vec::new();
13 let mut default_dirs = Vec::new();
14
15 if let Some(home) = std::env::var_os("HOME") {
17 let user_systemd = PathBuf::from(home).join(".config/systemd/user");
18 user_dirs.push(user_systemd);
19 }
20
21 user_dirs.push(PathBuf::from("/usr/lib/systemd/user"));
23 user_dirs.push(PathBuf::from("/etc/systemd/user"));
24 user_dirs.push(PathBuf::from("/usr/local/lib/systemd/user"));
25
26 system_dirs.push(PathBuf::from("/lib/systemd/system"));
28 system_dirs.push(PathBuf::from("/usr/lib/systemd/system"));
29 system_dirs.push(PathBuf::from("/etc/systemd/system"));
30 system_dirs.push(PathBuf::from("/usr/local/lib/systemd/system"));
31
32 default_dirs.push(PathBuf::from("/etc/systemd/system"));
33
34 Config {
35 default_dirs,
36 user_dirs,
37 system_dirs,
38 }
39}
40
41pub(super) fn scan_directory(dir: &Path) -> Result<Vec<ServiceRef>> {
42 let mut services = Vec::new();
43
44 if !dir.exists() {
45 return Ok(services);
46 }
47
48 let entries = fs::read_dir(dir)?;
49
50 for entry in entries {
51 let entry = entry?;
52 let path = entry.path();
53
54 if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
55 if matches!(
56 extension,
57 "service"
58 | "socket"
59 | "timer"
60 | "target"
61 | "mount"
62 | "automount"
63 | "swap"
64 | "path"
65 | "slice"
66 | "scope"
67 ) {
68 if let Ok(service) = parse_unit_file(&path) {
69 services.push(service);
70 }
71 }
72 }
73 }
74
75 Ok(services)
76}
77
78fn parse_unit_file(path: &Path) -> Result<ServiceRef> {
79 let name = path
82 .file_name()
83 .and_then(|s| s.to_str())
84 .unwrap_or("unknown")
85 .to_string();
86
87 let enabled = is_service_enabled(path, &name);
91
92 Ok(ServiceRef {
93 name,
94 path: path.to_string_lossy().to_string(),
95 enabled,
96 })
97}
98
99fn is_service_enabled(_path: &Path, name: &str) -> bool {
100 let wants_dirs = [
102 "/etc/systemd/system/multi-user.target.wants",
103 "/etc/systemd/system/graphical.target.wants",
104 "/etc/systemd/system/default.target.wants",
105 ];
106
107 for wants_dir in &wants_dirs {
108 let symlink_path = PathBuf::from(wants_dir).join(name);
109 if symlink_path.exists() {
110 return true;
111 }
112 }
113
114 let parent_dir = PathBuf::from("/etc/systemd/system");
116 let possible_symlink = parent_dir.join(name);
117 if possible_symlink.exists() && possible_symlink.is_symlink() {
118 return true;
119 }
120
121 false
122}
123
124pub fn get_service_details(name: &str) -> Result<FsServiceDetails> {
125 let service_ref = super::get_service(name)?;
127
128 let contents = fs::read_to_string(&service_ref.path)
130 .with_context(|| format!("Failed to read service file: {}", service_ref.path))?;
131
132 let service = parse_systemd(&contents)?;
133 let running = is_service_running(name)?;
134
135 Ok(FsServiceDetails {
136 running,
137 service,
138 enabled: service_ref.enabled,
139 path: service_ref.path,
140 })
141}
142
143pub fn get_service_file_path(name: &str) -> Result<String> {
144 let all_services = list_services(super::ListLevel::System)?;
145 let service = all_services
146 .iter()
147 .find(|s| s.name == name)
148 .ok_or_else(|| anyhow!("Service '{}' not found", name))?;
149 Ok(service.path.clone())
150}
151
152pub fn start_service(name: &str) -> Result<()> {
153 refresh_daemon()?;
155
156 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
158 let timer_name = format!("{}.timer", base_name);
159 let timer_path = PathBuf::from("/etc/systemd/system").join(&timer_name);
160
161 let unit_to_start = if timer_path.exists() {
162 &timer_name
164 } else {
165 name
166 };
167
168 let mut cmd = Command::new("systemctl");
169 cmd.args(["enable", "--now"]).arg(unit_to_start);
170 print_command(&cmd);
171 let output = cmd.output().context("Failed to execute systemctl")?;
172
173 if !output.status.success() {
174 let stderr = String::from_utf8_lossy(&output.stderr);
175 return Err(anyhow!("Failed to start '{}': {}", unit_to_start, stderr));
176 }
177
178 Ok(())
179}
180
181pub fn stop_service(name: &str) -> Result<()> {
182 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
184 let timer_name = format!("{}.timer", base_name);
185 let timer_path = PathBuf::from("/etc/systemd/system").join(&timer_name);
186
187 let unit_to_stop = if timer_path.exists() {
188 &timer_name
190 } else {
191 name
192 };
193
194 let mut cmd = Command::new("systemctl");
195 cmd.args(["disable", "--now"]).arg(unit_to_stop);
196 print_command(&cmd);
197 let output = cmd.output().context("Failed to execute systemctl")?;
198
199 if !output.status.success() {
200 let stderr = String::from_utf8_lossy(&output.stderr);
201 return Err(anyhow!("Failed to stop '{}': {}", unit_to_stop, stderr));
202 }
203
204 Ok(())
205}
206
207pub fn restart_service(name: &str) -> Result<()> {
208 refresh_daemon()?;
209 let mut cmd = Command::new("systemctl");
210 cmd.args(["restart"]).arg(name);
211 print_command(&cmd);
212 let output = cmd.output().context("Failed to execute systemctl")?;
213
214 if !output.status.success() {
215 let stderr = String::from_utf8_lossy(&output.stderr);
216 return Err(anyhow!("Failed to restart service '{}': {}", name, stderr));
217 }
218
219 Ok(())
220}
221
222pub fn create_service(details: &ServiceDetails) -> Result<()> {
223 let systemd_system_dir = PathBuf::from("/etc/systemd/system");
224
225 fs::create_dir_all(&systemd_system_dir).context("Failed to create systemd user directory")?;
227
228 let service_path = systemd_system_dir.join(format!("{}.service", details.name));
230 let service_content = generate_file(details)?;
231 fs::write(&service_path, service_content)
232 .with_context(|| format!("Failed to write unit file: {}", service_path.display()))?;
233
234 if details.schedule.is_some() {
236 let timer_path = systemd_system_dir.join(format!("{}.timer", details.name));
237 let timer_content = crate::systemd::generate_timer_file(details)?;
238 fs::write(&timer_path, timer_content)
239 .with_context(|| format!("Failed to write timer file: {}", timer_path.display()))?;
240 }
241
242 refresh_daemon()?;
244
245 Ok(())
246}
247
248pub fn is_service_running(name: &str) -> Result<bool> {
249 let mut cmd = Command::new("systemctl");
250 cmd.args(["is-active", "--quiet"]).arg(name);
251 print_command(&cmd);
252 let output = cmd.output().context("Failed to execute systemctl")?;
253
254 Ok(output.status.success())
255}
256
257pub fn show_service_logs(name: &str, lines: u32, follow: bool) -> Result<()> {
258 let mut cmd = Command::new("journalctl");
259 cmd.args(["-u", name]);
260
261 cmd.arg("-n").arg(lines.to_string());
263
264 if follow {
265 cmd.arg("-f");
266 }
267
268 cmd.arg("--no-pager");
270
271 print_command(&cmd);
272 let mut child = cmd
273 .spawn()
274 .context("Failed to execute journalctl command")?;
275
276 let status = child
277 .wait()
278 .context("Failed to wait for journalctl command")?;
279
280 if !status.success() {
281 return Err(anyhow!("Journalctl command failed with status: {}", status));
282 }
283
284 Ok(())
285}
286
287fn refresh_daemon() -> anyhow::Result<()> {
288 let mut cmd = Command::new("systemctl");
289 cmd.arg("daemon-reload");
290 print_command(&cmd);
291 cmd.status()
292 .context("Failed to execute systemctl daemon-reload")?;
293 Ok(())
294}
295
296pub fn has_timer(name: &str) -> bool {
298 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
299 let timer_path = PathBuf::from("/etc/systemd/system").join(format!("{}.timer", base_name));
300 timer_path.exists()
301}
302
303pub fn get_timer_next_trigger(name: &str) -> Result<Option<String>> {
305 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
306 let timer_name = format!("{}.timer", base_name);
307
308 let mut cmd = Command::new("systemctl");
309 cmd.args([
310 "show",
311 &timer_name,
312 "--property=NextElapseUSecRealtime",
313 "--value",
314 ]);
315 print_command(&cmd);
316 let output = cmd.output().context("Failed to execute systemctl")?;
317
318 if output.status.success() {
319 let next = String::from_utf8_lossy(&output.stdout).trim().to_string();
320 if !next.is_empty() && next != "n/a" {
321 return Ok(Some(next));
322 }
323 }
324 Ok(None)
325}
326
327pub fn is_timer_enabled(name: &str) -> bool {
329 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
330 let timer_name = format!("{}.timer", base_name);
331
332 let mut cmd = Command::new("systemctl");
333 cmd.args(["is-enabled", &timer_name]);
334 print_command(&cmd);
335
336 if let Ok(output) = cmd.output() {
337 let status = String::from_utf8_lossy(&output.stdout);
338 return status.trim() == "enabled";
339 }
340 false
341}