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 Claude Code process.
6
7use std::process::Command;
8
9/// Information about the Claude Code process
10#[derive(Debug, Clone)]
11pub struct ClaudeProcessInfo {
12    /// PID of the Claude Code (node) process
13    pub pid: i32,
14    /// Command name (e.g., "node", "claude")
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 Claude Code process.
64///
65/// Claude Code runs as a node process, so we look for that in our
66/// process ancestry.
67///
68/// Returns `None` if we can't find a node process in the ancestry.
69pub fn find_claude_process() -> Option<ClaudeProcessInfo> {
70    find_claude_process_from_pid(get_parent_pid()?)
71}
72
73/// Walk up from a given PID to find the Claude Code process.
74fn find_claude_process_from_pid(start_pid: i32) -> Option<ClaudeProcessInfo> {
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)
91        if is_framework_process(&info.comm) {
92            return Some(ClaudeProcessInfo {
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/// - Claude Code: runs as "node" or contains "claude"/"electron"
137/// - Codex: runs as "codex"
138/// - Gemini: runs as "gemini"
139/// - Cursor: runs as "Cursor" or "Cursor Helper"
140fn is_framework_process(comm: &str) -> bool {
141    let comm_lower = comm.to_lowercase();
142
143    comm_lower == "node"
144        || comm_lower == "codex"
145        || comm_lower == "gemini"
146        || comm_lower.contains("claude")
147        || comm_lower.contains("electron")
148        || comm_lower.contains("cursor")
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_is_process_alive_self() {
157        // Our own process should be alive
158        assert!(is_process_alive(std::process::id() as i32));
159    }
160
161    #[test]
162    fn test_is_process_alive_invalid_pid() {
163        // A non-existent PID should not be alive
164        assert!(!is_process_alive(999_999_999));
165    }
166
167    #[test]
168    fn test_get_parent_pid() -> Result<(), String> {
169        // We should always have a parent process
170        let ppid = get_parent_pid().ok_or("expected parent pid")?;
171        assert!(ppid > 0);
172        Ok(())
173    }
174
175    #[test]
176    fn test_get_process_info_self() {
177        // We should be able to get info about our own process
178        let pid = std::process::id() as i32;
179        let info = get_process_info(pid);
180        assert!(info.is_some(), "should get info for own process");
181    }
182
183    #[test]
184    fn test_get_process_info_invalid_pid() {
185        // Invalid PID should return None
186        let info = get_process_info(999_999_999);
187        assert!(info.is_none());
188    }
189}