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