1use anyhow::{Context, Result};
2
3use super::GlobalOpts;
4
5#[allow(clippy::unused_async)]
6pub async fn run(_global: &GlobalOpts) -> Result<()> {
7 let project_dir = std::env::current_dir().context("getting current directory")?;
8 let pid_path = project_dir.join(".oven").join("oven.pid");
9
10 let pid_str = std::fs::read_to_string(&pid_path)
11 .context("no detached process found (missing .oven/oven.pid)")?;
12 let pid = pid_str.trim().parse::<u32>().context("invalid PID in .oven/oven.pid")?;
13
14 let comm_output = std::process::Command::new("ps")
16 .args(["-p", &pid.to_string(), "-o", "comm="])
17 .output()
18 .context("checking process identity")?;
19 let comm = String::from_utf8_lossy(&comm_output.stdout).trim().to_string();
20 if !comm_output.status.success() || !comm.contains("oven") {
21 tracing::warn!(pid, comm = %comm, "PID is not an oven process, removing stale PID file");
22 std::fs::remove_file(&pid_path).ok();
23 anyhow::bail!("PID {pid} is not an oven process (found: {comm}). Removed stale PID file.");
24 }
25
26 let status = std::process::Command::new("kill")
28 .arg("-TERM")
29 .arg(pid.to_string())
30 .status()
31 .context("sending SIGTERM")?;
32
33 if !status.success() {
34 tracing::warn!(pid, "kill returned non-zero (process may already be stopped)");
35 }
36
37 let mut exited = false;
39 for _ in 0..50 {
40 let check = std::process::Command::new("kill").arg("-0").arg(pid.to_string()).status();
41 match check {
42 Ok(s) if !s.success() => {
43 exited = true;
44 break;
45 }
46 _ => std::thread::sleep(std::time::Duration::from_millis(100)),
47 }
48 }
49
50 if !exited {
52 tracing::warn!(pid, "process did not exit after SIGTERM, sending SIGKILL");
53 let _ = std::process::Command::new("kill").args(["-KILL", &pid.to_string()]).status();
54 std::thread::sleep(std::time::Duration::from_millis(500));
55 }
56
57 std::fs::remove_file(&pid_path).ok();
58 println!("stopped (pid {pid})");
59 Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64 #[test]
65 fn pid_parse_valid() {
66 let pid: u32 = "12345\n".trim().parse().unwrap();
67 assert_eq!(pid, 12345);
68 }
69
70 #[test]
71 fn pid_parse_invalid() {
72 let result = "not_a_pid".parse::<u32>();
73 assert!(result.is_err());
74 }
75
76 #[test]
77 fn missing_pid_file_gives_helpful_error() {
78 let dir = tempfile::tempdir().unwrap();
79 let path = dir.path().join(".oven").join("oven.pid");
80 let result = std::fs::read_to_string(&path);
81 assert!(result.is_err());
82 }
83}