serlib/platform/
linux.rs

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    // User-specific systemd directory
16    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 unit directories (global user services)
22    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 unit directories
27    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 _contents = fs::read_to_string(path)?;
80
81    let name = path
82        .file_name()
83        .and_then(|s| s.to_str())
84        .unwrap_or("unknown")
85        .to_string();
86
87    // Simple heuristic: if the file exists and is readable, consider it "enabled"
88    // In reality, we'd need to check symlinks in /etc/systemd/system/*.wants/ directories
89    // or parse the unit file more thoroughly
90    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    // Check common systemd target directories for symlinks
101    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    // Also check if there's a symlink in the same directory structure
115    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    // Find the service first
126    let service_ref = super::get_service(name)?;
127
128    // Parse the unit file for detailed information
129    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    // Reload systemd daemon to pick up any configuration changes
154    refresh_daemon()?;
155
156    // Check if this is a timer-based service
157    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        // Start and enable the timer, not the service
163        &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    // Check if this is a timer-based service
183    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        // Stop and disable the timer
189        &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    // Ensure the directory exists
226    fs::create_dir_all(&systemd_system_dir).context("Failed to create systemd user directory")?;
227
228    // Always create the service file
229    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 scheduled, also create timer file
235    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    // Reload systemd daemon
243    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    // Limit number of lines
262    cmd.arg("-n").arg(lines.to_string());
263
264    if follow {
265        cmd.arg("-f");
266    }
267
268    // Show output with colors and pager disabled for better integration
269    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
296/// Check if a service has an associated timer file.
297pub 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
303/// Get the next trigger time for a timer.
304pub 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
327/// Check if a timer is enabled.
328pub 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}