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 status = std::process::Command::new("kill")
16 .arg("-TERM")
17 .arg(pid.to_string())
18 .status()
19 .context("sending SIGTERM")?;
20
21 if !status.success() {
22 tracing::warn!(pid, "kill returned non-zero (process may already be stopped)");
24 }
25
26 for _ in 0..50 {
28 let check = std::process::Command::new("kill").arg("-0").arg(pid.to_string()).status();
29 match check {
30 Ok(s) if !s.success() => break, _ => std::thread::sleep(std::time::Duration::from_millis(100)),
32 }
33 }
34
35 std::fs::remove_file(&pid_path).ok();
36 println!("stopped (pid {pid})");
37 Ok(())
38}
39
40#[cfg(test)]
41mod tests {
42 #[test]
43 fn pid_parse_valid() {
44 let pid: u32 = "12345\n".trim().parse().unwrap();
45 assert_eq!(pid, 12345);
46 }
47
48 #[test]
49 fn pid_parse_invalid() {
50 let result = "not_a_pid".parse::<u32>();
51 assert!(result.is_err());
52 }
53
54 #[test]
55 fn missing_pid_file_gives_helpful_error() {
56 let dir = tempfile::tempdir().unwrap();
57 let path = dir.path().join(".oven").join("oven.pid");
58 let result = std::fs::read_to_string(&path);
59 assert!(result.is_err());
60 }
61}