mi6_core/context/
process.rs

1//! Process tree utilities for finding and checking process status.
2//!
3//! This module provides cross-platform utilities for process management,
4//! including checking if processes are alive and walking the process tree
5//! to find the framework process (Claude, Codex, Gemini, Cursor, etc.).
6
7use std::process::Command;
8
9/// Information about the framework process (Claude, Codex, Gemini, Cursor, etc.)
10#[derive(Debug, Clone)]
11pub struct FrameworkProcessInfo {
12    /// PID of the framework process
13    pub pid: i32,
14    /// Command name (e.g., "node", "claude", "codex", "gemini")
15    pub comm: String,
16}
17
18/// Check if a process with the given PID is still running.
19///
20/// This uses the `ps` command to check process existence, which works
21/// on both macOS and Linux.
22///
23/// # Examples
24///
25/// ```
26/// use mi6_core::is_process_alive;
27///
28/// // Our own process should be alive
29/// let own_pid = std::process::id() as i32;
30/// assert!(is_process_alive(own_pid));
31///
32/// // A non-existent PID should not be alive
33/// assert!(!is_process_alive(999999999));
34/// ```
35pub fn is_process_alive(pid: i32) -> bool {
36    Command::new("ps")
37        .args(["-p", &pid.to_string()])
38        .stdout(std::process::Stdio::null())
39        .stderr(std::process::Stdio::null())
40        .status()
41        .is_ok_and(|status| status.success())
42}
43
44/// Get the parent process ID.
45///
46/// This is more reliable than the PPID environment variable,
47/// which is a shell variable that isn't exported to child processes.
48///
49/// # Examples
50///
51/// ```
52/// use mi6_core::get_parent_pid;
53///
54/// // We should always have a parent process
55/// let ppid = get_parent_pid();
56/// assert!(ppid.is_some());
57/// ```
58pub fn get_parent_pid() -> Option<i32> {
59    let pid = std::process::id() as i32;
60    get_process_info(pid).map(|info| info.ppid)
61}
62
63/// Walk up the process tree to find the framework process.
64///
65/// Looks for Claude, Codex, Gemini, Cursor, or other supported frameworks
66/// in the process ancestry.
67///
68/// Returns `None` if we can't find a framework process in the ancestry.
69pub fn find_framework_process() -> Option<FrameworkProcessInfo> {
70    find_framework_process_from_pid(get_parent_pid()?)
71}
72
73/// Walk up from a given PID to find the framework process.
74fn find_framework_process_from_pid(start_pid: i32) -> Option<FrameworkProcessInfo> {
75    let mut current_pid = start_pid;
76    let mut visited = std::collections::HashSet::new();
77
78    // Walk up the process tree (limit iterations to prevent infinite loops)
79    for _ in 0..50 {
80        if current_pid <= 1 || visited.contains(&current_pid) {
81            break;
82        }
83        visited.insert(current_pid);
84
85        // Get process info
86        let Some(info) = get_process_info(current_pid) else {
87            break;
88        };
89
90        // Check if this is a framework process (Claude, Codex, Gemini, Cursor)
91        if is_framework_process(&info.comm) {
92            return Some(FrameworkProcessInfo {
93                pid: current_pid,
94                comm: info.comm,
95            });
96        }
97
98        // Move to parent
99        current_pid = info.ppid;
100    }
101
102    None
103}
104
105struct ProcessInfo {
106    ppid: i32,
107    comm: String,
108}
109
110/// Get process info using ps command (works on macOS and Linux)
111fn get_process_info(pid: i32) -> Option<ProcessInfo> {
112    // Get PPID and command name in one call
113    let output = Command::new("ps")
114        .args(["-o", "ppid=,comm=", "-p", &pid.to_string()])
115        .output()
116        .ok()?;
117
118    if !output.status.success() {
119        return None;
120    }
121
122    let stdout = String::from_utf8_lossy(&output.stdout);
123    let line = stdout.trim();
124
125    // Parse "  123 node" format
126    let mut parts = line.split_whitespace();
127    let ppid: i32 = parts.next()?.parse().ok()?;
128    let comm = parts.next()?.to_string();
129
130    Some(ProcessInfo { ppid, comm })
131}
132
133/// Check if a process name looks like a supported AI coding framework.
134///
135/// Matches:
136/// - Amp: runs as "amp"
137/// - Claude Code: runs as "node" or contains "claude"/"electron"
138/// - Codex: runs as "codex"
139/// - Gemini: runs as "gemini"
140/// - Cursor: runs as "Cursor" or "Cursor Helper"
141fn is_framework_process(comm: &str) -> bool {
142    let comm_lower = comm.to_lowercase();
143
144    comm_lower == "amp"
145        || comm_lower == "node"
146        || comm_lower == "codex"
147        || comm_lower == "gemini"
148        || comm_lower.contains("claude")
149        || comm_lower.contains("electron")
150        || comm_lower.contains("cursor")
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_is_process_alive_self() {
159        // Our own process should be alive
160        assert!(is_process_alive(std::process::id() as i32));
161    }
162
163    #[test]
164    fn test_is_process_alive_invalid_pid() {
165        // A non-existent PID should not be alive
166        assert!(!is_process_alive(999_999_999));
167    }
168
169    #[test]
170    fn test_get_parent_pid() -> Result<(), String> {
171        // We should always have a parent process
172        let ppid = get_parent_pid().ok_or("expected parent pid")?;
173        assert!(ppid > 0);
174        Ok(())
175    }
176
177    #[test]
178    fn test_get_process_info_self() {
179        // We should be able to get info about our own process
180        let pid = std::process::id() as i32;
181        let info = get_process_info(pid);
182        assert!(info.is_some(), "should get info for own process");
183    }
184
185    #[test]
186    fn test_get_process_info_invalid_pid() {
187        // Invalid PID should return None
188        let info = get_process_info(999_999_999);
189        assert!(info.is_none());
190    }
191}