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" => {
244            return Some("claude".to_string())
245        }
246        "codex" | "codex-cli" | "openai-codex" => return Some("codex".to_string()),
247        "gemini" | "gemini-cli" | "google-gemini" => return Some("gemini".to_string()),
248        _ => {}
249    }
250
251    // Partial matches for variations (exclude claude-desktop to avoid confusion)
252    // Also support claude-code in partial matches
253    if (clean_name.contains("claude") || clean_name.contains("claude-code"))
254        && !clean_name.contains("claude-desktop")
255    {
256        return Some("claude".to_string());
257    }
258    if clean_name.contains("codex") {
259        return Some("codex".to_string());
260    }
261    if clean_name.contains("gemini") {
262        return Some("gemini".to_string());
263    }
264
265    // NPM-based AI CLI processes - only for node-based AI CLIs
266    if clean_name == "node" {
267        if let Some(ai_type) = detect_npm_ai_cli_type(pid) {
268            return Some(ai_type);
269        }
270    }
271
272    None
273}
274
275/// Enhanced detection for NPM AI CLI processes using command line inspection
276pub fn detect_npm_ai_cli_type(pid: u32) -> Option<String> {
277    #[cfg(unix)]
278    {
279        detect_npm_ai_cli_type_unix(pid)
280    }
281
282    #[cfg(windows)]
283    {
284        detect_npm_ai_cli_type_windows(pid)
285    }
286}
287
288#[cfg(unix)]
289fn detect_npm_ai_cli_type_unix(pid: u32) -> Option<String> {
290    get_command_line(pid)
291        .and_then(|cmd| analyze_cmdline_for_ai_cli(&cmd))
292        .or_else(|| Some("node".to_string()))
293}
294
295fn build_ai_cli_process_info(pid: u32) -> Option<AiCliProcessInfo> {
296    let process_name = get_process_name(pid)?;
297    let ai_type = get_ai_cli_type(pid, &process_name)?;
298    let mut info = AiCliProcessInfo::new(pid, ai_type).with_process_name(process_name);
299
300    if let Some(cmdline) = get_command_line(pid) {
301        let npm_flag = is_npm_command_line(&cmdline);
302        info = info
303            .with_command_line(cmdline)
304            .with_is_npm_package(npm_flag);
305    }
306
307    if let Some(path) = get_executable_path(pid) {
308        info = info.with_executable_path(Some(path));
309    }
310
311    info.validate().ok()?;
312    Some(info)
313}
314
315fn is_npm_command_line(cmdline: &str) -> bool {
316    let cmd = cmdline.to_lowercase();
317    cmd.contains("npm exec") || cmd.contains("npx ")
318}
319
320#[cfg(unix)]
321fn get_command_line(pid: u32) -> Option<String> {
322    use std::fs::File;
323    use std::io::Read;
324
325    let cmdline_path = format!("/proc/{pid}/cmdline");
326    let mut file = File::open(cmdline_path).ok()?;
327    let mut raw = String::new();
328    file.read_to_string(&mut raw).ok()?;
329    let cleaned = raw.replace('\0', " ").trim().to_string();
330    if cleaned.is_empty() {
331        None
332    } else {
333        Some(cleaned)
334    }
335}
336
337#[cfg(windows)]
338fn get_command_line(pid: u32) -> Option<String> {
339    read_process_info_windows(pid, true)
340        .ok()
341        .and_then(|info| info.cmdline)
342        .map(|cmd| cmd.join(" "))
343        .filter(|cmd| !cmd.trim().is_empty())
344}
345
346#[cfg(unix)]
347fn get_executable_path(pid: u32) -> Option<PathBuf> {
348    std::fs::read_link(format!("/proc/{pid}/exe")).ok()
349}
350
351#[cfg(windows)]
352fn get_executable_path(pid: u32) -> Option<PathBuf> {
353    read_process_info_windows(pid, false)
354        .ok()
355        .and_then(|info| info.executable_path)
356}
357
358#[cfg(windows)]
359fn detect_npm_ai_cli_type_windows(pid: u32) -> Option<String> {
360    get_command_line(pid)
361        .and_then(|cmd| analyze_cmdline_for_ai_cli(&cmd))
362        .or_else(|| Some("node".to_string()))
363}
364
365/// Analyze command line string to identify specific AI CLI type
366#[allow(dead_code)]
367fn analyze_cmdline_for_ai_cli(cmdline: &str) -> Option<String> {
368    let cmdline_lower = cmdline.to_lowercase();
369
370    // Direct AI CLI execution via npm/npx
371    if cmdline_lower.contains("claude-cli") || cmdline_lower.contains("@anthropic-ai/claude-cli") {
372        return Some("claude".to_string());
373    }
374    if cmdline_lower.contains("claude-code") {
375        return Some("claude".to_string());
376    }
377    if cmdline_lower.contains("codex-cli") {
378        return Some("codex".to_string());
379    }
380    if cmdline_lower.contains("gemini-cli") || cmdline_lower.contains("@google/generative-ai-cli") {
381        return Some("gemini".to_string());
382    }
383
384    // npm exec patterns
385    if cmdline_lower.contains("npm exec") {
386        if cmdline_lower.contains("@anthropic-ai/claude") {
387            return Some("claude".to_string());
388        }
389        if cmdline_lower.contains("codex") {
390            return Some("codex".to_string());
391        }
392        if cmdline_lower.contains("gemini") {
393            return Some("gemini".to_string());
394        }
395    }
396
397    // npx patterns
398    if cmdline_lower.contains("npx") {
399        if cmdline_lower.contains("@anthropic-ai/claude") {
400            return Some("claude".to_string());
401        }
402        if cmdline_lower.contains("codex") {
403            return Some("codex".to_string());
404        }
405        if cmdline_lower.contains("gemini") {
406            return Some("gemini".to_string());
407        }
408    }
409
410    // Node.js with module paths
411    if cmdline_lower.contains("node_modules") {
412        if cmdline_lower.contains("claude-cli") || cmdline_lower.contains("claude-code") {
413            return Some("claude".to_string());
414        }
415        if cmdline_lower.contains("codex-cli") {
416            return Some("codex".to_string());
417        }
418        if cmdline_lower.contains("gemini-cli") {
419            return Some("gemini".to_string());
420        }
421    }
422
423    // Generic Node.js detection if no specific AI CLI found
424    Some("node".to_string())
425}
426
427/// Get the process tree from a given PID up to the root parent
428fn get_process_tree_internal(pid: u32) -> Result<ProcessTreeInfo, ProcessTreeError> {
429    let mut chain = Vec::new();
430
431    // Start with the current process
432    let mut current_pid = pid;
433    chain.push(current_pid);
434    let mut ai_cli_info: Option<AiCliProcessInfo> = None;
435
436    // Traverse up the process tree
437    for _ in 0..50 {
438        match get_parent_pid(current_pid)? {
439            Some(parent_pid) => {
440                if parent_pid == current_pid || parent_pid == 0 {
441                    // We've reached the root or found a loop
442                    break;
443                }
444
445                chain.push(parent_pid);
446                if ai_cli_info.is_none() {
447                    ai_cli_info = build_ai_cli_process_info(parent_pid);
448                }
449                current_pid = parent_pid;
450
451                // Check if we've reached a known root process
452                if is_root_process(parent_pid) {
453                    break;
454                }
455            }
456            None => {
457                break;
458            }
459        }
460    }
461
462    let info = ProcessTreeInfo::new(chain).with_ai_cli_process(ai_cli_info);
463    info.validate()
464        .map_err(|err| ProcessTreeError::Validation(err.to_string()))?;
465    Ok(info)
466}
467
468pub fn get_process_tree(pid: u32) -> AgenticResult<ProcessTreeInfo> {
469    get_process_tree_internal(pid).map_err(AgenticWardenError::from)
470}
471
472/// Get the parent PID for a given process using platform-specific methods
473fn get_parent_pid(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
474    #[cfg(windows)]
475    {
476        get_parent_pid_windows(pid)
477    }
478
479    #[cfg(unix)]
480    {
481        get_parent_pid_unix(pid)
482    }
483}
484
485/// Windows-specific implementation backed by a cached sysinfo snapshot
486#[cfg(windows)]
487fn get_parent_pid_windows(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
488    if pid == 0 {
489        return Ok(None);
490    }
491    match read_process_info_windows(pid, false) {
492        Ok(info) => Ok(info.parent.filter(|parent| *parent != pid)),
493        Err(ProcessTreeError::ProcessNotFound(_)) => Ok(None),
494        Err(err) => Err(err),
495    }
496}
497
498/// Unix-specific implementation using psutil
499#[cfg(unix)]
500fn get_parent_pid_unix(pid: u32) -> Result<Option<u32>, ProcessTreeError> {
501    let process = Process::new(pid.into())?;
502    match process.ppid() {
503        Ok(parent_pid_opt) => {
504            // ppid() returns Option<u32>, already the correct type
505            Ok(parent_pid_opt)
506        }
507        Err(err) => Err(err.into()),
508    }
509}
510
511/// Check if a PID represents a root process
512fn is_root_process(pid: u32) -> bool {
513    #[cfg(windows)]
514    {
515        // Windows root processes
516        pid == 0 || pid == 4 || pid == 1
517    }
518
519    #[cfg(unix)]
520    {
521        // Unix root processes
522        pid == 1 || pid == 0
523    }
524}
525
526/// Get process name for a given PID (platform-specific)
527#[allow(dead_code)]
528pub fn get_process_name(pid: u32) -> Option<String> {
529    #[cfg(windows)]
530    {
531        get_process_name_windows(pid)
532    }
533
534    #[cfg(unix)]
535    {
536        get_process_name_unix(pid)
537    }
538}
539
540/// Windows process name implementation using sysinfo
541#[cfg(windows)]
542#[allow(dead_code)]
543fn get_process_name_windows(pid: u32) -> Option<String> {
544    read_process_info_windows(pid, false)
545        .ok()
546        .and_then(|info| info.name)
547}
548
549/// Unix process name implementation using psutil
550#[cfg(unix)]
551fn get_process_name_unix(pid: u32) -> Option<String> {
552    match Process::new(pid.into()) {
553        Ok(process) => match process.name() {
554            Ok(name) => Some(name),
555            Err(_) => None,
556        },
557        Err(_) => None,
558    }
559}
560
561/// Check if two processes have the same root parent
562#[allow(dead_code)]
563pub fn same_root_parent(pid1: u32, pid2: u32) -> AgenticResult<bool> {
564    let tree1 = get_process_tree(pid1)?;
565    let tree2 = get_process_tree(pid2)?;
566
567    match (tree1.root_parent_pid, tree2.root_parent_pid) {
568        (Some(root1), Some(root2)) => Ok(root1 == root2),
569        _ => Ok(false),
570    }
571}
572
573/// Get direct parent PID using fallback methods
574#[allow(dead_code)]
575pub fn get_direct_parent_pid_fallback() -> Option<u32> {
576    get_parent_pid(std::process::id()).ok().flatten()
577}
578
579fn process_tree_issue(operation: &str, message: impl Into<String>) -> AgenticWardenError {
580    AgenticWardenError::Process {
581        message: message.into(),
582        command: format!("process_tree::{operation}"),
583        source: None,
584    }
585}
586
587#[allow(dead_code)]
588fn process_tree_issue_with_source(
589    operation: &str,
590    message: impl Into<String>,
591    source: impl std::error::Error + Send + Sync + 'static,
592) -> AgenticWardenError {
593    AgenticWardenError::Process {
594        message: message.into(),
595        command: format!("process_tree::{operation}"),
596        source: Some(Box::new(source)),
597    }
598}
599
600impl From<ProcessTreeError> for AgenticWardenError {
601    fn from(err: ProcessTreeError) -> Self {
602        match err {
603            #[cfg(unix)]
604            ProcessTreeError::ProcessInfo(source) => {
605                process_tree_issue_with_source("info", "Failed to get process information", source)
606            }
607            #[cfg(windows)]
608            ProcessTreeError::ProcessInfo(message) => process_tree_issue(
609                "info",
610                format!("Failed to get process information: {message}"),
611            ),
612            ProcessTreeError::ProcessNotFound(pid) => {
613                process_tree_issue("lookup", format!("Process {pid} not found"))
614            }
615            ProcessTreeError::PermissionDenied(pid) => process_tree_issue(
616                "permission",
617                format!("Permission denied accessing process {pid}"),
618            ),
619            ProcessTreeError::UnsupportedPlatform => process_tree_issue(
620                "platform",
621                "Unsupported platform for process tree inspection",
622            ),
623            ProcessTreeError::Validation(message) => process_tree_issue("validate", message),
624        }
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[cfg(windows)]
633    use sysinfo::System;
634
635    #[test]
636    fn test_current_process_tree() {
637        let result = ProcessTreeInfo::current();
638        assert!(result.is_ok());
639
640        let tree = result.unwrap();
641        assert!(!tree.process_chain.is_empty());
642        assert!(tree.depth >= 1);
643        assert_eq!(tree.process_chain[0], std::process::id());
644    }
645
646    #[test]
647    fn test_same_root_parent_current() {
648        let current_pid = std::process::id();
649        let result = same_root_parent(current_pid, current_pid);
650        assert!(result.is_ok());
651        assert!(result.unwrap());
652    }
653
654    #[test]
655    fn test_direct_parent_fallback() {
656        let parent_pid = get_direct_parent_pid_fallback();
657        assert!(parent_pid.is_some(), "Should be able to get parent PID");
658
659        let parent = parent_pid.unwrap();
660        assert!(parent > 0, "Parent PID should be positive");
661    }
662
663    #[test]
664    fn test_root_process_detection() {
665        #[cfg(windows)]
666        {
667            assert!(is_root_process(0), "PID 0 should be root on Windows");
668            assert!(is_root_process(4), "PID 4 should be root on Windows");
669        }
670
671        #[cfg(unix)]
672        {
673            assert!(is_root_process(1), "PID 1 should be root on Unix");
674        }
675    }
676
677    #[test]
678    fn test_process_chain_validity() {
679        let tree = ProcessTreeInfo::current().expect("Failed to get process tree");
680
681        // Verify all PIDs are positive
682        for pid in &tree.process_chain {
683            assert!(*pid > 0, "All PIDs in process chain should be positive");
684        }
685
686        // Verify no duplicates (except possible root)
687        let mut seen = std::collections::HashSet::new();
688        for pid in &tree.process_chain {
689            assert!(
690                !seen.contains(pid),
691                "Process chain should not contain duplicate PIDs"
692            );
693            seen.insert(*pid);
694        }
695    }
696
697    #[test]
698    fn test_process_name_retrieval() {
699        let current_pid = std::process::id();
700        let process_name = get_process_name(current_pid);
701
702        // Process name should be available
703        if let Some(name) = process_name {
704            println!("Current process name: {}", name);
705            assert!(!name.is_empty(), "Process name should not be empty");
706        }
707    }
708
709    #[test]
710    fn test_analyze_cmdline_detects_specific_cli() {
711        let claude_cmd = "node ./node_modules/@anthropic-ai/claude-cli/bin/run.js ask";
712        assert_eq!(
713            analyze_cmdline_for_ai_cli(claude_cmd),
714            Some("claude".to_string())
715        );
716
717        let codex_cmd = "npx codex-cli chat --model gpt-4";
718        assert_eq!(
719            analyze_cmdline_for_ai_cli(codex_cmd),
720            Some("codex".to_string())
721        );
722
723        let gemini_cmd = "npm exec @google/generative-ai-cli -- text";
724        assert_eq!(
725            analyze_cmdline_for_ai_cli(gemini_cmd),
726            Some("gemini".to_string())
727        );
728    }
729
730    #[test]
731    fn test_analyze_cmdline_defaults_to_node() {
732        let generic_cmd = "node ./scripts/custom-runner.js";
733        assert_eq!(
734            analyze_cmdline_for_ai_cli(generic_cmd),
735            Some("node".to_string())
736        );
737    }
738
739    #[cfg(unix)]
740    #[test]
741    fn test_unix_psutil_integration() {
742        // Test that we can use psutil on Unix systems
743        let current_pid = std::process::id();
744        let process = Process::new(current_pid.into());
745        assert!(
746            process.is_ok(),
747            "Should be able to access current process via psutil"
748        );
749
750        if let Ok(proc) = process {
751            // Test getting parent PID via psutil
752            let ppid_result = proc.ppid();
753            assert!(
754                ppid_result.is_ok(),
755                "Should be able to get parent PID via psutil"
756            );
757
758            // Test getting process name via psutil
759            let name_result = proc.name();
760            println!("Process name via psutil: {:?}", name_result);
761        }
762    }
763
764    #[cfg(windows)]
765    #[test]
766    fn test_windows_sysinfo_integration() {
767        // Test that we can use sysinfo on Windows
768        let mut system = System::new();
769        system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
770
771        let current_pid = std::process::id();
772        let found_process = system
773            .processes()
774            .values()
775            .find(|p| p.pid().as_u32() == current_pid);
776
777        assert!(
778            found_process.is_some(),
779            "Should be able to find current process via sysinfo"
780        );
781
782        if let Some(process) = found_process {
783            println!(
784                "Current process found: {} (PID: {})",
785                process.name().to_string_lossy(),
786                process.pid().as_u32()
787            );
788            assert!(
789                !process.name().is_empty(),
790                "Process name should not be empty"
791            );
792        }
793    }
794
795    #[cfg(windows)]
796    #[test]
797    fn test_windows_process_info_cache_roundtrip() {
798        let pid = std::process::id();
799        let info_a =
800            read_process_info_windows(pid, false).expect("Process info should be available");
801        assert!(info_a.name.is_some());
802
803        let info_b = read_process_info_windows(pid, false)
804            .expect("Process info should be cached and still available");
805        assert!(info_b.name.is_some());
806    }
807}