1use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18use anyhow::{Context, Result};
19use sysinfo::{Pid, Signal, System};
20
21pub fn pid_path(settings_dir: &Path) -> PathBuf {
25 settings_dir.join("gateway.pid")
26}
27
28pub fn log_path(settings_dir: &Path) -> PathBuf {
30 settings_dir.join("logs").join("gateway.log")
31}
32
33pub fn write_pid(settings_dir: &Path, pid: u32) -> Result<()> {
35 let path = pid_path(settings_dir);
36 if let Some(parent) = path.parent() {
37 fs::create_dir_all(parent)?;
38 }
39 fs::write(&path, pid.to_string())
40 .with_context(|| format!("Failed to write PID file {}", path.display()))
41}
42
43pub fn read_pid(settings_dir: &Path) -> Option<u32> {
45 let path = pid_path(settings_dir);
46 fs::read_to_string(&path)
47 .ok()
48 .and_then(|s| s.trim().parse().ok())
49}
50
51pub fn remove_pid(settings_dir: &Path) {
53 let path = pid_path(settings_dir);
54 let _ = fs::remove_file(&path);
55}
56
57pub fn is_process_alive(pid: u32) -> bool {
59 let mut sys = System::new();
60 sys.refresh_processes(
61 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
62 true,
63 );
64 sys.process(Pid::from_u32(pid)).is_some()
65}
66
67#[derive(Debug, Clone)]
71pub enum DaemonStatus {
72 Running { pid: u32 },
74 Stale { pid: u32 },
76 Stopped,
78}
79
80pub fn status(settings_dir: &Path) -> DaemonStatus {
82 match read_pid(settings_dir) {
83 Some(pid) => {
84 if is_process_alive(pid) {
85 DaemonStatus::Running { pid }
86 } else {
87 DaemonStatus::Stale { pid }
88 }
89 }
90 None => DaemonStatus::Stopped,
91 }
92}
93
94pub fn start(
100 settings_dir: &Path,
101 port: u16,
102 bind: &str,
103 extra_args: &[String],
104 model_api_key: Option<&str>,
105 vault_password: Option<&str>,
106 tls_cert: Option<&Path>,
107 tls_key: Option<&Path>,
108) -> Result<u32> {
109 if let DaemonStatus::Running { pid } = status(settings_dir) {
111 anyhow::bail!("Gateway is already running (PID {})", pid);
112 }
113
114 remove_pid(settings_dir);
116
117 let gateway_bin = resolve_gateway_binary()?;
119
120 let log = log_path(settings_dir);
122 if let Some(parent) = log.parent() {
123 fs::create_dir_all(parent)?;
124 }
125
126 let log_file = fs::File::create(&log)
127 .with_context(|| format!("Failed to create gateway log at {}", log.display()))?;
128 let log_stderr = log_file
129 .try_clone()
130 .context("Failed to clone log file handle")?;
131
132 let mut cmd = Command::new(&gateway_bin);
133 cmd.arg("run")
134 .arg("--port")
135 .arg(port.to_string())
136 .arg("--bind")
137 .arg(bind)
138 .arg("--settings-dir")
139 .arg(settings_dir)
140 .stdout(log_file)
141 .stderr(log_stderr);
142
143 if let Some(key) = model_api_key {
146 cmd.env("RUSTYCLAW_MODEL_API_KEY", key);
147 }
148
149 if let Some(pw) = vault_password {
151 cmd.env("RUSTYCLAW_VAULT_PASSWORD", pw);
152 }
153
154 if let Some(cert) = tls_cert {
156 cmd.arg("--tls-cert").arg(cert);
157 }
158 if let Some(key) = tls_key {
159 cmd.arg("--tls-key").arg(key);
160 }
161
162 for a in extra_args {
163 cmd.arg(a);
164 }
165
166 detach_child(&mut cmd);
168
169 let child = cmd
170 .spawn()
171 .with_context(|| format!("Failed to spawn {}", gateway_bin.display()))?;
172
173 let pid = child.id();
174 write_pid(settings_dir, pid)?;
175
176 Ok(pid)
177}
178
179pub fn stop(settings_dir: &Path) -> Result<StopResult> {
181 match status(settings_dir) {
182 DaemonStatus::Running { pid } => {
183 kill_process(pid)?;
184 for _ in 0..20 {
186 std::thread::sleep(std::time::Duration::from_millis(100));
187 if !is_process_alive(pid) {
188 remove_pid(settings_dir);
189 return Ok(StopResult::Stopped { pid });
190 }
191 }
192 remove_pid(settings_dir);
195 Ok(StopResult::Stopped { pid })
196 }
197 DaemonStatus::Stale { pid } => {
198 remove_pid(settings_dir);
199 Ok(StopResult::WasStale { pid })
200 }
201 DaemonStatus::Stopped => Ok(StopResult::WasNotRunning),
202 }
203}
204
205#[derive(Debug)]
206pub enum StopResult {
207 Stopped { pid: u32 },
208 WasStale { pid: u32 },
209 WasNotRunning,
210}
211
212fn kill_process(pid: u32) -> Result<()> {
215 let sysinfo_pid = Pid::from_u32(pid);
216 let mut sys = System::new();
217 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[sysinfo_pid]), true);
218 let process = sys
219 .process(sysinfo_pid)
220 .context(format!("Process {} not found", pid))?;
221
222 if !process.kill_with(Signal::Term).unwrap_or(false) {
223 process.kill();
226 }
227 Ok(())
228}
229
230#[cfg(unix)]
232fn detach_child(cmd: &mut Command) {
233 use std::os::unix::process::CommandExt;
234 cmd.process_group(0);
237}
238
239#[cfg(windows)]
240fn detach_child(cmd: &mut Command) {
241 use std::os::windows::process::CommandExt;
242 cmd.creation_flags(0x0000_0208);
244}
245
246#[cfg(not(any(unix, windows)))]
247fn detach_child(_cmd: &mut Command) {
248 }
250
251fn resolve_gateway_binary() -> Result<PathBuf> {
255 let name = if cfg!(windows) {
256 "rustyclaw-gateway.exe"
257 } else {
258 "rustyclaw-gateway"
259 };
260
261 if let Ok(current_exe) = std::env::current_exe() {
263 if let Some(dir) = current_exe.parent() {
264 let candidate = dir.join(name);
265 if candidate.is_file() {
266 return Ok(candidate);
267 }
268 }
269 }
270
271 if let Ok(path) = which::which(name) {
273 return Ok(path);
274 }
275
276 anyhow::bail!(
277 "Could not find the `rustyclaw-gateway` binary.\n\
278 Make sure it is installed or built (`cargo build`) and on your PATH."
279 )
280}