1use super::{list_services, Config, ServiceRef};
2pub use crate::systemd::generate_file;
3use crate::systemd::parse_systemd;
4use crate::{run_command, run_command_status, spawn_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 output = run_command(Command::new("systemctl")
169 .args(["enable", "--now"])
170 .arg(unit_to_start))
171 .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 output = run_command(Command::new("systemctl")
195 .args(["disable", "--now"])
196 .arg(unit_to_stop))
197 .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 output = run_command(Command::new("systemctl")
210 .args(["restart"])
211 .arg(name))
212 .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 output = run_command(Command::new("systemctl")
250 .args(["is-active", "--quiet"])
251 .arg(name))
252 .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 let mut child = spawn_command(&mut cmd)
272 .context("Failed to execute journalctl command")?;
273
274 let status = child
275 .wait()
276 .context("Failed to wait for journalctl command")?;
277
278 if !status.success() {
279 return Err(anyhow!("Journalctl command failed with status: {}", status));
280 }
281
282 Ok(())
283}
284
285fn refresh_daemon() -> anyhow::Result<()> {
286 run_command_status(Command::new("systemctl")
287 .arg("daemon-reload"))
288 .context("Failed to execute systemctl daemon-reload")?;
289 Ok(())
290}
291
292pub fn has_timer(name: &str) -> bool {
294 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
295 let timer_path = PathBuf::from("/etc/systemd/system").join(format!("{}.timer", base_name));
296 timer_path.exists()
297}
298
299pub fn get_timer_next_trigger(name: &str) -> Result<Option<String>> {
301 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
302 let timer_name = format!("{}.timer", base_name);
303
304 let output = run_command(Command::new("systemctl")
305 .args([
306 "show",
307 &timer_name,
308 "--property=NextElapseUSecRealtime",
309 "--value",
310 ]))
311 .context("Failed to execute systemctl")?;
312
313 if output.status.success() {
314 let next = String::from_utf8_lossy(&output.stdout).trim().to_string();
315 if !next.is_empty() && next != "n/a" {
316 return Ok(Some(next));
317 }
318 }
319 Ok(None)
320}
321
322pub fn is_timer_enabled(name: &str) -> bool {
324 let base_name = name.trim_end_matches(".service").trim_end_matches(".timer");
325 let timer_name = format!("{}.timer", base_name);
326
327 let output = run_command(Command::new("systemctl")
328 .args(["is-enabled", &timer_name]));
329
330 if let Ok(output) = output {
331 let status = String::from_utf8_lossy(&output.stdout);
332 return status.trim() == "enabled";
333 }
334 false
335}