agentic_warden/core/
process_tree.rs

1//! Process tree tracking module
2//!
3//! This module provides functionality to traverse the process tree from the current
4//! process up to the root parent, enabling process isolation based on ancestry.
5//!
6//! Platform strategy:
7//! - Linux/macOS: Use psutil for comprehensive process information
8//! - Windows: Use sysinfo library for cross-platform process information
9
10#[cfg(unix)]
11use psutil::process::Process;
12
13#[cfg(windows)]
14use parking_lot::RwLock;
15#[cfg(windows)]
16use std::cell::RefCell;
17#[cfg(windows)]
18use std::collections::HashMap;
19#[cfg(windows)]
20use std::time::{Duration, Instant};
21#[cfg(windows)]
22use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
23
24use crate::core::models::{AiCliProcessInfo, ProcessTreeInfo};
25use crate::error::{AgenticResult, AgenticWardenError};
26use std::path::PathBuf;
27use std::sync::OnceLock;
28use thiserror::Error;
29
30// Global cache for root parent PID - computed once per process lifetime
31static ROOT_PARENT_PID_CACHE: OnceLock<u32> = OnceLock::new();
32#[cfg(windows)]
33static PROCESS_INFO_CACHE: OnceLock<RwLock<HashMap<u32, CacheEntry>>> = OnceLock::new();
34#[cfg(windows)]
35const PROCESS_CACHE_TTL: Duration = Duration::from_millis(750);
36#[cfg(windows)]
37thread_local! {
38    static THREAD_SYSINFO: RefCell<SysinfoState> = RefCell::new(SysinfoState::new());
39}
40
41#[derive(Error, Debug)]
42pub enum ProcessTreeError {
43    #[cfg(unix)]
44    #[error("Failed to get process information: {0}")]
45    ProcessInfo(#[from] psutil::Error),
46    #[cfg(windows)]
47    #[allow(dead_code)]
48    #[error("Failed to get process information: {0}")]
49    ProcessInfo(String),
50    #[allow(dead_code)]
51    #[error("Process not found: {0}")]
52    ProcessNotFound(u32),
53    #[allow(dead_code)]
54    #[error("Permission denied accessing process: {0}")]
55    PermissionDenied(u32),
56    #[allow(dead_code)]
57    #[error("Unsupported platform")]
58    UnsupportedPlatform,
59    #[error("Process tree validation failed: {0}")]
60    Validation(String),
61}
62
63// Add support for psutil::process::ProcessError conversion
64#[cfg(unix)]
65impl From<psutil::process::ProcessError> for ProcessTreeError {
66    fn from(err: psutil::process::ProcessError) -> Self {
67        // Convert ProcessError to ProcessTreeError using Debug formatting
68        use std::io;
69        ProcessTreeError::ProcessInfo(psutil::Error::from(io::Error::other(format!("{:?}", err))))
70    }
71}
72
73#[cfg(windows)]
74#[derive(Clone, Debug)]
75struct ProcessInfo {
76    parent: Option<u32>,
77    name: Option<String>,
78    cmdline: Option<Vec<String>>,
79    executable_path: Option<PathBuf>,
80}
81
82#[cfg(windows)]
83#[derive(Clone, Debug)]
84struct CacheEntry {
85    info: ProcessInfo,
86    expires_at: Instant,
87}
88
89#[cfg(windows)]
90#[derive(Debug)]
91struct SysinfoState {
92    system: System,
93}
94
95#[cfg(windows)]
96impl SysinfoState {
97    fn new() -> Self {
98        let mut system = System::new();
99        system.refresh_processes(ProcessesToUpdate::All, true);
100        Self { system }
101    }
102
103    fn snapshot(&mut self, pid: u32, include_cmdline: bool) -> Option<ProcessInfo> {
104        let sys_pid = Pid::from_u32(pid);
105        let pid_list = [sys_pid];
106        let refresh_kind = if include_cmdline {
107            ProcessRefreshKind::everything()
108        } else {
109            ProcessRefreshKind::new()
110        };
111        let _ = self.system.refresh_processes_specifics(
112            ProcessesToUpdate::Some(&pid_list),
113            true,
114            refresh_kind,
115        );
116        if self.system.process(sys_pid).is_none() {
117            self.system.refresh_processes(ProcessesToUpdate::All, true);
118        }
119        self.system.process(sys_pid).map(|process| {
120            let parent = process.parent().map(|p| p.as_u32());
121            let name = Some(process.name().to_string_lossy().into_owned());
122            let cmdline = if include_cmdline {
123                let args: Vec<String> = process
124                    .cmd()
125                    .iter()
126                    .map(|arg| arg.to_string_lossy().into_owned())
127                    .collect();
128                if args.is_empty() {
129                    None
130                } else {
131                    Some(args)
132                }
133            } else {
134                None
135            };
136            let executable_path = process.exe().map(|path| path.to_path_buf());
137            ProcessInfo {
138                parent,
139                name,
140                cmdline,
141                executable_path,
142            }
143        })
144    }
145}
146
147#[cfg(windows)]
148fn process_info_cache() -> &'static RwLock<HashMap<u32, CacheEntry>> {
149    PROCESS_INFO_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
150}
151
152#[cfg(windows)]
153fn read_process_info_windows(
154    pid: u32,
155    require_cmdline: bool,
156) -> Result<ProcessInfo, ProcessTreeError> {
157    if pid == 0 {
158        return Ok(ProcessInfo {
159            parent: None,
160            name: Some("System Idle Process".to_string()),
161            cmdline: None,
162            executable_path: None,
163        });
164    }
165
166    let now = Instant::now();
167    {
168        let cache_guard = process_info_cache().read();
169        if let Some(entry) = cache_guard.get(&pid) {
170            if entry.expires_at > now && (!require_cmdline || entry.info.cmdline.is_some()) {
171                return Ok(entry.info.clone());
172            }
173        }
174    }
175
176    let snapshot = THREAD_SYSINFO
177        .with(|state| state.borrow_mut().snapshot(pid, require_cmdline))
178        .ok_or(ProcessTreeError::ProcessNotFound(pid))?;
179
180    {
181        let mut cache_guard = process_info_cache().write();
182        cache_guard.insert(
183            pid,
184            CacheEntry {
185                info: snapshot.clone(),
186                expires_at: now + PROCESS_CACHE_TTL,
187            },
188        );
189    }
190
191    Ok(snapshot)
192}
193
194impl ProcessTreeInfo {
195    /// Get the current process tree information
196    pub fn current() -> AgenticResult<Self> {
197        get_process_tree(std::process::id())
198    }
199}
200
201/// Get the root parent process ID for the current process (cached)
202/// This function computes the root parent PID only once per process lifetime
203/// It finds the nearest AI CLI process in the process tree, not just any parent
204pub fn get_root_parent_pid_cached() -> AgenticResult<u32> {
205    let current_pid = std::process::id();
206
207    // Use a simple caching approach - compute if not set
208    if let Some(&cached_pid) = ROOT_PARENT_PID_CACHE.get() {
209        Ok(cached_pid)
210    } else {
211        let ai_root_pid = find_ai_cli_root_parent(current_pid)?;
212        // Set the cache (ignore if another thread set it first)
213        let _ = ROOT_PARENT_PID_CACHE.set(ai_root_pid);
214        Ok(ai_root_pid)
215    }
216}
217
218/// Find the nearest AI CLI process in the process tree
219/// If no AI CLI process is found, falls back to the traditional root parent
220pub fn find_ai_cli_root_parent(pid: u32) -> AgenticResult<u32> {
221    let process_tree = get_process_tree(pid)?;
222
223    process_tree
224        .get_ai_cli_root()
225        .ok_or_else(|| ProcessTreeError::ProcessNotFound(pid).into())
226}
227
228/// Get the specific AI CLI type from a process name and command line
229/// Returns: Some("claude"), Some("codex"), Some("gemini"), or None
230fn get_ai_cli_type(pid: u32, process_name: &str) -> Option<String> {
231    let name_lower = process_name.to_lowercase();
232
233    // Remove .exe extension on Windows for comparison
234    let clean_name = if cfg!(windows) && name_lower.ends_with(".exe") {
235        &name_lower[..name_lower.len() - 4]
236    } else {
237        &name_lower
238    };
239
240    // Native AI CLI processes - exact matches first
241    // Added claude-code for Claude Code support
242    match clean_name {
243        "claude" | "claude-cli" | "anthropic-claude" | "claude-code" => return Some("claude".to_string()),
244        "codex" | "codex-cli" | "openai-codex" => return Some("codex".to_string()),
245        "gemini" | "gemini-cli" | "google-gemini" => return Some("gemini".to_string()),
246        _ => {}
247    }
248
249    // Partial matches for variations (exclude claude-desktop to avoid confusion)
250    // Also support claude-code in partial matches
251    if (clean_name.contains("claude") || clean_name.contains("claude-code")) && !clean_name.contains("claude-desktop") {
252        return Some("claude".to_string());
253    }
254    if clean_name.contains("codex") {
255        return Some("codex".to_string());
256    }
257    if clean_name.contains("gemini") {
258        return Some("gemini".to_string());
259    }
260
261    // NPM-based AI CLI processes - only for node-based AI CLIs
262    if clean_name == "node" {
263        if let Some(ai_type) = detect_npm_ai_cli_type(pid) {
264            return Some(ai_type);
265        }
266    }
267
268    None
269}
270
271/// Enhanced detection for NPM AI CLI processes using command line inspection
272pub fn detect_npm_ai_cli_type(pid: u32) -> Option<String> {
273    #[cfg(unix)]
274    {
275        detect_npm_ai_cli_type_unix(pid)
276    }
277
278    #[cfg(windows)]
279    {
280        detect_npm_ai_cli_type_windows(pid)
281    }
282}
283
284#[cfg(unix)]
285fn detect_npm_ai_cli_type_unix(pid: u32) -> Option<String> {
286    get_command_line(pid)
287        .and_then(|cmd| analyze_cmdline_for_ai_cli(&cmd))
288        .or_else(|| Some("node".to_string()))
289}
290
291fn build_ai_cli_process_info(pid: u32) -> Option<AiCliProcessInfo> {
292    let process_name = get_process_name(pid)?;
293    let ai_type = get_ai_cli_type(pid, &process_name)?;
294    let mut info = AiCliProcessInfo::new(pid, ai_type).with_process_name(process_name);
295
296    if let Some(cmdline) = get_command_line(pid) {
297        let npm_flag = is_npm_command_line(&cmdline);
298        info = info
299            .with_command_line(cmdline)
300            .with_is_npm_package(npm_flag);
301    }
302
303    if let Some(path) = get_executable_path(pid) {
304        info = info.with_executable_path(Some(path));
305    }
306
307    info.validate().ok()?;
308    Some(info)
309}
310
311fn is_npm_command_line(cmdline: &str) -> bool {
312    let cmd = cmdline.to_lowercase();
313    cmd.contains("npm exec") || cmd.contains("npx ")
314}
315
316#[cfg(unix)]
317fn get_command_line(pid: u32) -> Option<String> {
318    use std::fs::File;
319    use std::io::Read;
320
321    let cmdline_path = format!("/proc/{pid}/cmdline");
322    let mut file = File::open(cmdline_path).ok()?;
323    let mut raw = String::new();
324    file.read_to_string(&mut raw).ok()?;
325    let cleaned = raw.replace('\0', " ").trim().to_string();
326    if cleaned.is_empty() {
327        None
328    } else {
329        Some(cleaned)
330    }
331}
332
333#[cfg(windows)]
334fn get_command_line(pid: u32) -> Option<String> {
335    read_process_info_windows(pid, true)
336        .ok()
337        .and_then(|info| info.cmdline)
338        .map(|cmd| cmd.join(" "))
339        .filter(|cmd| !cmd.trim().is_empty())
340}
341
342#[cfg(unix)]
343fn get_executable_path(pid: u32) -> Option<PathBuf> {
344    std::fs::read_link(format!("/proc/{pid}/exe")).ok()
345}
346
347#[cfg(windows)]
348fn get_executable_path(pid: u32) -> Option<PathBuf> {
349    read_process_info_windows(pid, false)
350        .ok()
351        .and_then(|info| info.executable_path)
352}
353
354#[cfg(windows)]
355fn detect_npm_ai_cli_type_windows(pid: u32) -> Option<String> {
356    get_command_line(pid)
357        .and_then(|cmd| analyze_cmdline_for_ai_cli(&cmd))
358        .or_else(|| Some("node".to_string()))
359}
360
361/// Analyze command line string to identify specific AI CLI type
362#[allow(dead_code)]
363fn analyze_cmdline_for_ai_cli(cmdline: &str) -> Option<String> {
364    let cmdline_lower = cmdline.to_lowercase();
365
366    // Direct AI CLI execution via npm/npx
367    if cmdline_lower.contains("claude-cli") || cmdline_lower.contains("@anthropic-ai/claude-cli") {
368        return Some("claude".to_string());
369    }
370    if cmdline_lower.contains("claude-code") {
371        return Some("claude".to_string());
372    }
373    if cmdline_lower.contains("codex-cli") {
374        return Some("codex".to_string());
375    }
376    if cmdline_lower.contains("gemini-cli") || cmdline_lower.contains("@google/generative-ai-cli") {
377        return Some("gemini".to_string());
378    }
379
380    // npm exec patterns
381    if cmdline_lower.contains("npm exec") {
382        if cmdline_lower.contains("@anthropic-ai/claude") {
383            return Some("claude".to_string());
384        }
385        if cmdline_lower.contains("codex") {
386            return Some("codex".to_string());
387        }
388        if cmdline_lower.contains("gemini") {
389            return Some("gemini".to_string());
390        }
391    }
392
393    // npx patterns
394    if cmdline_lower.contains("npx") {
395        if cmdline_lower.contains("@anthropic-ai/claude") {
396            return Some("claude".to_string());
397        }
398        if cmdline_lower.contains("codex") {
399            return Some("codex".to_string());
400        }
401        if cmdline_lower.contains("gemini") {
402            return Some("gemini".to_string());
403        }
404    }
405
406    // Node.js with module paths
407    if cmdline_lower.contains("node_modules") {
408        if cmdline_lower.contains("claude-cli") || cmdline_lower.contains("claude-code") {
409            return Some("claude".to_string());
410        }
411        if cmdline_lower.contains("codex-cli") {
412            return Some("codex".to_string());
413        }
414        if cmdline_lower.contains("gemini-cli") {
415            return Some("gemini".to_string());
416        }
417    }
418
419    // Generic Node.js detection if no specific AI CLI found
420    Some("node".to_string())
421}
422
423/// Get the process tree from a given PID up to the root parent
424fn get_process_tree_internal(pid: u32) -> Result<ProcessTreeInfo, ProcessTreeError> {
425    let mut chain = Vec::new();
426
427    // Start with the current process
428    let mut current_pid = pid;
429    chain.push(current_pid);
430    let mut ai_cli_info: Option<AiCliProcessInfo> = None;
431
432    // Traverse up the process tree
433    for _ in 0..50 {
434        match get_parent_pid(current_pid)? {
435            Some(parent_pid) => {
436                if parent_pid == current_pid || parent_pid == 0 {
437                    // We've reached the root or found a loop
438                    break;
439                }
440
441                chain.push(parent_pid);
442                if ai_cli_info.is_none() {
443                    ai_cli_info = build_ai_cli_process_info(parent_pid);
444                }
445                current_pid = parent_pid;
446
447                // Check if we've reached a known root process
448                if is_root_process(parent_pid) {
449                    break;
450                }
451            }
452            None => {
453                break;
454            }
455        }
456    }
457
458    let info = ProcessTreeInfo::new(chain).with_ai_cli_process(ai_cli_info);
459    info.validate()
460        .map_err(|err| ProcessTreeError::Validation(err.to_string()))?;
461    Ok(info)
462}
463
464pub fn get_process_tree(pid: u32) -> AgenticResult<ProcessTreeInfo> {
465    get_process_tree_internal(pid).map_err(AgenticWardenError::from)
466}
467
468/// Get the parent PID for a given process using platform-specific methods
469fn get_parent_pid(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
470    #[cfg(windows)]
471    {
472        get_parent_pid_windows(pid)
473    }
474
475    #[cfg(unix)]
476    {
477        get_parent_pid_unix(pid)
478    }
479}
480
481/// Windows-specific implementation backed by a cached sysinfo snapshot
482#[cfg(windows)]
483fn get_parent_pid_windows(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
484    if pid == 0 {
485        return Ok(None);
486    }
487    match read_process_info_windows(pid, false) {
488        Ok(info) => Ok(info.parent.filter(|parent| *parent != pid)),
489        Err(ProcessTreeError::ProcessNotFound(_)) => Ok(None),
490        Err(err) => Err(err),
491    }
492}
493
494/// Unix-specific implementation using psutil
495#[cfg(unix)]
496fn get_parent_pid_unix(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
497    let process = Process::new(pid.into())?;
498    match process.ppid() {
499        Ok(parent_pid_opt) => {
500            // ppid() returns Option<u32>, already the correct type
501            Ok(parent_pid_opt)
502        }
503        Err(err) => Err(err.into()),
504    }
505}
506
507/// Check if a PID represents a root process
508fn is_root_process(pid: u32) -> bool {
509    #[cfg(windows)]
510    {
511        // Windows root processes
512        pid == 0 || pid == 4 || pid == 1
513    }
514
515    #[cfg(unix)]
516    {
517        // Unix root processes
518        pid == 1 || pid == 0
519    }
520}
521
522/// Get process name for a given PID (platform-specific)
523#[allow(dead_code)]
524pub fn get_process_name(pid: u32) -> Option<String> {
525    #[cfg(windows)]
526    {
527        get_process_name_windows(pid)
528    }
529
530    #[cfg(unix)]
531    {
532        get_process_name_unix(pid)
533    }
534}
535
536/// Windows process name implementation using sysinfo
537#[cfg(windows)]
538#[allow(dead_code)]
539fn get_process_name_windows(pid: u32) -> Option<String> {
540    read_process_info_windows(pid, false)
541        .ok()
542        .and_then(|info| info.name)
543}
544
545/// Unix process name implementation using psutil
546#[cfg(unix)]
547fn get_process_name_unix(pid: u32) -> Option<String> {
548    match Process::new(pid.into()) {
549        Ok(process) => match process.name() {
550            Ok(name) => Some(name),
551            Err(_) => None,
552        },
553        Err(_) => None,
554    }
555}
556
557/// Check if two processes have the same root parent
558#[allow(dead_code)]
559pub fn same_root_parent(pid1: u32, pid2: u32) -> AgenticResult<bool> {
560    let tree1 = get_process_tree(pid1)?;
561    let tree2 = get_process_tree(pid2)?;
562
563    match (tree1.root_parent_pid, tree2.root_parent_pid) {
564        (Some(root1), Some(root2)) => Ok(root1 == root2),
565        _ => Ok(false),
566    }
567}
568
569/// Get direct parent PID using fallback methods
570#[allow(dead_code)]
571pub fn get_direct_parent_pid_fallback() -> Option<u32> {
572    get_parent_pid(std::process::id()).ok().flatten()
573}
574
575fn process_tree_issue(operation: &str, message: impl Into<String>) -> AgenticWardenError {
576    AgenticWardenError::Process {
577        message: message.into(),
578        command: format!("process_tree::{operation}"),
579        source: None,
580    }
581}
582
583#[allow(dead_code)]
584fn process_tree_issue_with_source(
585    operation: &str,
586    message: impl Into<String>,
587    source: impl std::error::Error + Send + Sync + 'static,
588) -> AgenticWardenError {
589    AgenticWardenError::Process {
590        message: message.into(),
591        command: format!("process_tree::{operation}"),
592        source: Some(Box::new(source)),
593    }
594}
595
596impl From<ProcessTreeError> for AgenticWardenError {
597    fn from(err: ProcessTreeError) -> Self {
598        match err {
599            #[cfg(unix)]
600            ProcessTreeError::ProcessInfo(source) => {
601                process_tree_issue_with_source("info", "Failed to get process information", source)
602            }
603            #[cfg(windows)]
604            ProcessTreeError::ProcessInfo(message) => process_tree_issue(
605                "info",
606                format!("Failed to get process information: {message}"),
607            ),
608            ProcessTreeError::ProcessNotFound(pid) => {
609                process_tree_issue("lookup", format!("Process {pid} not found"))
610            }
611            ProcessTreeError::PermissionDenied(pid) => process_tree_issue(
612                "permission",
613                format!("Permission denied accessing process {pid}"),
614            ),
615            ProcessTreeError::UnsupportedPlatform => process_tree_issue(
616                "platform",
617                "Unsupported platform for process tree inspection",
618            ),
619            ProcessTreeError::Validation(message) => process_tree_issue("validate", message),
620        }
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627
628    #[cfg(windows)]
629    use sysinfo::System;
630
631    #[test]
632    fn test_current_process_tree() {
633        let result = ProcessTreeInfo::current();
634        assert!(result.is_ok());
635
636        let tree = result.unwrap();
637        assert!(!tree.process_chain.is_empty());
638        assert!(tree.depth >= 1);
639        assert_eq!(tree.process_chain[0], std::process::id());
640    }
641
642    #[test]
643    fn test_same_root_parent_current() {
644        let current_pid = std::process::id();
645        let result = same_root_parent(current_pid, current_pid);
646        assert!(result.is_ok());
647        assert!(result.unwrap());
648    }
649
650    #[test]
651    fn test_direct_parent_fallback() {
652        let parent_pid = get_direct_parent_pid_fallback();
653        assert!(parent_pid.is_some(), "Should be able to get parent PID");
654
655        let parent = parent_pid.unwrap();
656        assert!(parent > 0, "Parent PID should be positive");
657    }
658
659    #[test]
660    fn test_root_process_detection() {
661        #[cfg(windows)]
662        {
663            assert!(is_root_process(0), "PID 0 should be root on Windows");
664            assert!(is_root_process(4), "PID 4 should be root on Windows");
665        }
666
667        #[cfg(unix)]
668        {
669            assert!(is_root_process(1), "PID 1 should be root on Unix");
670        }
671    }
672
673    #[test]
674    fn test_process_chain_validity() {
675        let tree = ProcessTreeInfo::current().expect("Failed to get process tree");
676
677        // Verify all PIDs are positive
678        for pid in &tree.process_chain {
679            assert!(*pid > 0, "All PIDs in process chain should be positive");
680        }
681
682        // Verify no duplicates (except possible root)
683        let mut seen = std::collections::HashSet::new();
684        for pid in &tree.process_chain {
685            assert!(
686                !seen.contains(pid),
687                "Process chain should not contain duplicate PIDs"
688            );
689            seen.insert(*pid);
690        }
691    }
692
693    #[test]
694    fn test_process_name_retrieval() {
695        let current_pid = std::process::id();
696        let process_name = get_process_name(current_pid);
697
698        // Process name should be available
699        if let Some(name) = process_name {
700            println!("Current process name: {}", name);
701            assert!(!name.is_empty(), "Process name should not be empty");
702        }
703    }
704
705    #[test]
706    fn test_analyze_cmdline_detects_specific_cli() {
707        let claude_cmd = "node ./node_modules/@anthropic-ai/claude-cli/bin/run.js ask";
708        assert_eq!(
709            analyze_cmdline_for_ai_cli(claude_cmd),
710            Some("claude".to_string())
711        );
712
713        let codex_cmd = "npx codex-cli chat --model gpt-4";
714        assert_eq!(
715            analyze_cmdline_for_ai_cli(codex_cmd),
716            Some("codex".to_string())
717        );
718
719        let gemini_cmd = "npm exec @google/generative-ai-cli -- text";
720        assert_eq!(
721            analyze_cmdline_for_ai_cli(gemini_cmd),
722            Some("gemini".to_string())
723        );
724    }
725
726    #[test]
727    fn test_analyze_cmdline_defaults_to_node() {
728        let generic_cmd = "node ./scripts/custom-runner.js";
729        assert_eq!(
730            analyze_cmdline_for_ai_cli(generic_cmd),
731            Some("node".to_string())
732        );
733    }
734
735    #[cfg(unix)]
736    #[test]
737    fn test_unix_psutil_integration() {
738        // Test that we can use psutil on Unix systems
739        let current_pid = std::process::id();
740        let process = Process::new(current_pid.into());
741        assert!(
742            process.is_ok(),
743            "Should be able to access current process via psutil"
744        );
745
746        if let Ok(proc) = process {
747            // Test getting parent PID via psutil
748            let ppid_result = proc.ppid();
749            assert!(
750                ppid_result.is_ok(),
751                "Should be able to get parent PID via psutil"
752            );
753
754            // Test getting process name via psutil
755            let name_result = proc.name();
756            println!("Process name via psutil: {:?}", name_result);
757        }
758    }
759
760    #[cfg(windows)]
761    #[test]
762    fn test_windows_sysinfo_integration() {
763        // Test that we can use sysinfo on Windows
764        let mut system = System::new();
765        system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
766
767        let current_pid = std::process::id();
768        let found_process = system
769            .processes()
770            .values()
771            .find(|p| p.pid().as_u32() == current_pid);
772
773        assert!(
774            found_process.is_some(),
775            "Should be able to find current process via sysinfo"
776        );
777
778        if let Some(process) = found_process {
779            println!(
780                "Current process found: {} (PID: {})",
781                process.name().to_string_lossy(),
782                process.pid().as_u32()
783            );
784            assert!(
785                !process.name().is_empty(),
786                "Process name should not be empty"
787            );
788        }
789    }
790
791    #[cfg(windows)]
792    #[test]
793    fn test_windows_process_info_cache_roundtrip() {
794        let pid = std::process::id();
795        let info_a =
796            read_process_info_windows(pid, false).expect("Process info should be available");
797        assert!(info_a.name.is_some());
798
799        let info_b = read_process_info_windows(pid, false)
800            .expect("Process info should be cached and still available");
801        assert!(info_b.name.is_some());
802    }
803}