auths_cli/commands/agent/
process.rs1#[cfg(not(unix))]
8use anyhow::anyhow;
9use anyhow::{Context, Result};
10use std::fs;
11use std::path::Path;
12use std::time::Duration;
13
14#[cfg(unix)]
15use nix::sys::signal::{self, Signal};
16#[cfg(unix)]
17use nix::unistd::Pid;
18
19pub fn write_pid_file(path: &Path, pid: u32) -> Result<()> {
30 crate::core::fs::write_sensitive_file(path, pid.to_string())
31 .with_context(|| format!("Failed to write PID file: {:?}", path))
32}
33
34pub fn read_pid_file(path: &Path) -> Result<Option<u32>> {
46 if !path.exists() {
47 return Ok(None);
48 }
49
50 let content =
51 fs::read_to_string(path).with_context(|| format!("Failed to read PID file: {:?}", path))?;
52
53 let pid: u32 = content
54 .trim()
55 .parse()
56 .with_context(|| format!("Invalid PID in file: {}", content.trim()))?;
57
58 Ok(Some(pid))
59}
60
61#[cfg(unix)]
76pub fn is_process_running(pid: u32) -> bool {
77 signal::kill(Pid::from_raw(pid as i32), None).is_ok()
78}
79
80#[cfg(not(unix))]
81pub fn is_process_running(_pid: u32) -> bool {
82 false
83}
84
85#[cfg(unix)]
100pub fn socket_is_connectable(path: &Path) -> bool {
101 std::os::unix::net::UnixStream::connect(path).is_ok()
102}
103
104#[cfg(not(unix))]
105pub fn socket_is_connectable(_path: &Path) -> bool {
106 false
107}
108
109#[cfg(unix)]
126pub fn spawn_detached(args: &[&str], log_path: &Path) -> Result<u32> {
127 use std::os::unix::process::CommandExt;
128 use std::process::Command;
129
130 let exe = std::env::current_exe().context("Failed to get current executable path")?;
131
132 let log_file = fs::File::create(log_path)
133 .with_context(|| format!("Failed to create log file: {:?}", log_path))?;
134 let log_file_err = log_file
135 .try_clone()
136 .context("Failed to clone log file handle")?;
137
138 let mut cmd = Command::new(&exe);
139 for arg in args {
140 cmd.arg(arg);
141 }
142 cmd.stdout(log_file).stderr(log_file_err);
143
144 unsafe {
146 cmd.pre_exec(|| {
147 nix::unistd::setsid().map_err(std::io::Error::other)?;
148 Ok(())
149 });
150 }
151
152 let child = cmd.spawn().context("Failed to spawn daemon process")?;
153 Ok(child.id())
154}
155
156#[cfg(not(unix))]
157pub fn spawn_detached(_args: &[&str], _log_path: &Path) -> Result<u32> {
158 Err(anyhow!(
159 "Daemonization not supported on this platform. Use --foreground."
160 ))
161}
162
163#[cfg(unix)]
175pub fn terminate_process(pid: u32, timeout: Duration) -> Result<()> {
176 signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM)
177 .with_context(|| format!("Failed to send SIGTERM to PID {}", pid))?;
178
179 let start = std::time::Instant::now();
180 while start.elapsed() < timeout {
181 if !is_process_running(pid) {
182 return Ok(());
183 }
184 std::thread::sleep(Duration::from_millis(100));
185 }
186
187 if is_process_running(pid) {
188 eprintln!("Process did not terminate gracefully, sending SIGKILL...");
189 let _ = signal::kill(Pid::from_raw(pid as i32), Signal::SIGKILL);
190 }
191
192 Ok(())
193}
194
195#[cfg(not(unix))]
196pub fn terminate_process(_pid: u32, _timeout: Duration) -> Result<()> {
197 Err(anyhow!("Stopping agent not supported on this platform"))
198}
199
200pub fn cleanup_stale_files(paths: &[&Path]) {
210 for path in paths {
211 let _ = fs::remove_file(path);
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use tempfile::TempDir;
219
220 #[test]
221 fn write_and_read_pid_file_round_trip() {
222 let dir = TempDir::new().unwrap();
223 let pid_path = dir.path().join("test.pid");
224
225 write_pid_file(&pid_path, 42).unwrap();
226 let read_back = read_pid_file(&pid_path).unwrap();
227 assert_eq!(read_back, Some(42));
228 }
229
230 #[test]
231 fn read_pid_file_missing_returns_none() {
232 let dir = TempDir::new().unwrap();
233 let pid_path = dir.path().join("nonexistent.pid");
234 assert_eq!(read_pid_file(&pid_path).unwrap(), None);
235 }
236
237 #[test]
238 fn read_pid_file_invalid_content_errors() {
239 let dir = TempDir::new().unwrap();
240 let pid_path = dir.path().join("bad.pid");
241 fs::write(&pid_path, "not-a-number").unwrap();
242 assert!(read_pid_file(&pid_path).is_err());
243 }
244
245 #[test]
246 fn cleanup_stale_files_removes_existing() {
247 let dir = TempDir::new().unwrap();
248 let f1 = dir.path().join("a");
249 let f2 = dir.path().join("b");
250 fs::write(&f1, "x").unwrap();
251 fs::write(&f2, "y").unwrap();
252
253 cleanup_stale_files(&[&f1, &f2]);
254
255 assert!(!f1.exists());
256 assert!(!f2.exists());
257 }
258
259 #[test]
260 fn cleanup_stale_files_ignores_missing() {
261 let dir = TempDir::new().unwrap();
262 let missing = dir.path().join("gone");
263 cleanup_stale_files(&[&missing]);
264 }
265
266 #[cfg(unix)]
267 #[test]
268 fn is_process_running_detects_current_process() {
269 assert!(is_process_running(std::process::id()));
270 }
271
272 #[cfg(unix)]
273 #[test]
274 fn is_process_running_false_for_nonexistent() {
275 assert!(!is_process_running(999_999_999));
276 }
277}