weft_core/service_manager/
linux.rs1use 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 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 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}