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(¤t_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}