Skip to main content

weft_core/service_manager/
linux.rs

1use super::{PlatformServiceManager, ServiceInstallOptions, ServiceStatus};
2use std::path::PathBuf;
3use std::process::Command;
4
5const SYSTEMD_DIR: &str = "/etc/systemd/system";
6
7pub struct LinuxServiceManager;
8
9impl PlatformServiceManager for LinuxServiceManager {
10    fn install(&self, opts: &ServiceInstallOptions) -> anyhow::Result<()> {
11        let unit_path = unit_path(&opts.service_name);
12        let binary = opts.binary_path.to_string_lossy();
13        let config_dir = opts.config_dir.to_string_lossy();
14        let data_dir = opts.data_dir.to_string_lossy();
15        let working_dir = opts
16            .binary_path
17            .parent()
18            .map(|p| p.to_string_lossy().to_string())
19            .unwrap_or_else(|| "/".to_string());
20
21        let unit = format!(
22            "[Unit]\n\
23             Description=WEFT Core AI agent runtime service\n\
24             After=network.target\n\
25             \n\
26             [Service]\n\
27             Type=simple\n\
28             ExecStart={binary} --config-dir {config_dir} --data-dir {data_dir}\n\
29             WorkingDirectory={working_dir}\n\
30             Restart=on-failure\n\
31             RestartSec=5\n\
32             StandardOutput=journal\n\
33             StandardError=journal\n\
34             \n\
35             [Install]\n\
36             WantedBy=multi-user.target\n"
37        );
38
39        std::fs::create_dir_all(SYSTEMD_DIR)
40            .map_err(|e| anyhow::anyhow!("cannot create systemd dir: {}", e))?;
41        std::fs::write(&unit_path, unit)
42            .map_err(|e| anyhow::anyhow!("cannot write unit file {:?}: {}", unit_path, e))?;
43        println!("Wrote unit file: {}", unit_path.display());
44
45        run_cmd("systemctl", &["daemon-reload"])?;
46        run_cmd("systemctl", &["enable", &opts.service_name])?;
47        self.start(&opts.service_name)?;
48        println!("Service '{}' installed and started.", opts.service_name);
49        Ok(())
50    }
51
52    fn uninstall(&self, service_name: &str) -> anyhow::Result<()> {
53        let _ = self.stop(service_name);
54        let _ = run_cmd("systemctl", &["disable", service_name]);
55        let unit = unit_path(service_name);
56        if unit.exists() {
57            std::fs::remove_file(&unit)
58                .map_err(|e| anyhow::anyhow!("cannot remove unit file: {}", e))?;
59        }
60        let _ = run_cmd("systemctl", &["daemon-reload"]);
61        println!("Service '{}' uninstalled.", service_name);
62        Ok(())
63    }
64
65    fn start(&self, service_name: &str) -> anyhow::Result<()> {
66        run_cmd("systemctl", &["start", service_name])?;
67        println!("Service '{}' started.", service_name);
68        Ok(())
69    }
70
71    fn stop(&self, service_name: &str) -> anyhow::Result<()> {
72        run_cmd("systemctl", &["stop", service_name])?;
73        println!("Service '{}' stopped.", service_name);
74        Ok(())
75    }
76
77    fn status(&self, service_name: &str) -> anyhow::Result<ServiceStatus> {
78        let unit = unit_path(service_name);
79        let installed = unit.exists();
80        if !installed {
81            return Ok(ServiceStatus {
82                installed: false,
83                running: false,
84                pid: None,
85                binary_path: None,
86            });
87        }
88
89        let out = Command::new("systemctl")
90            .args(["is-active", service_name])
91            .output()
92            .map_err(|e| anyhow::anyhow!("systemctl is-active failed: {}", e))?;
93        let running = String::from_utf8_lossy(&out.stdout).trim() == "active";
94
95        // Parse ExecStart from unit file to get binary path
96        let binary_path = std::fs::read_to_string(&unit).ok().and_then(|content| {
97            content
98                .lines()
99                .find(|l| l.starts_with("ExecStart="))
100                .and_then(|l| l.strip_prefix("ExecStart="))
101                .and_then(|l| l.split_whitespace().next())
102                .map(PathBuf::from)
103        });
104
105        // Get PID if running
106        let pid = if running {
107            Command::new("systemctl")
108                .args(["show", "-p", "MainPID", "--value", service_name])
109                .output()
110                .ok()
111                .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<u32>().ok())
112                .filter(|&p| p > 0)
113        } else {
114            None
115        };
116
117        Ok(ServiceStatus {
118            installed: true,
119            running,
120            pid,
121            binary_path,
122        })
123    }
124}
125
126fn unit_path(service_name: &str) -> PathBuf {
127    PathBuf::from(SYSTEMD_DIR).join(format!("{}.service", service_name))
128}
129
130fn run_cmd(cmd: &str, args: &[&str]) -> anyhow::Result<()> {
131    let out = Command::new(cmd)
132        .args(args)
133        .output()
134        .map_err(|e| anyhow::anyhow!("{} failed: {}", cmd, e))?;
135    if !out.status.success() {
136        let stderr = String::from_utf8_lossy(&out.stderr);
137        let stdout = String::from_utf8_lossy(&out.stdout);
138        anyhow::bail!(
139            "{} {} failed:\n{}\n{}",
140            cmd,
141            args.join(" "),
142            stdout.trim(),
143            stderr.trim()
144        );
145    }
146    Ok(())
147}