Skip to main content

oven_cli/cli/
off.rs

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    // Verify the PID belongs to an oven process before killing
15    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    // Send SIGTERM via the kill command (avoids unsafe libc calls)
27    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    // Wait briefly for the process to exit
38    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    // SIGKILL fallback if process didn't respond to SIGTERM
51    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}