Skip to main content

codex_helper_core/fleet/
process_scan.rs

1use serde::{Deserialize, Serialize};
2use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
3
4use super::model::FleetProcessSummary;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
7pub struct FleetProcessInfo {
8    pub pid: u32,
9    pub name: String,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub command: Option<String>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub cwd: Option<String>,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub started_at_ms: Option<u64>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
19pub struct FleetProcessScan {
20    pub processes: Vec<FleetProcessInfo>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub error: Option<String>,
23}
24
25impl FleetProcessScan {
26    pub fn summary(&self) -> FleetProcessSummary {
27        FleetProcessSummary {
28            scan_available: self.error.is_none(),
29            codex_like_processes: self.processes.len(),
30            error: self.error.clone(),
31        }
32    }
33}
34
35pub fn scan_codex_processes() -> FleetProcessScan {
36    let mut system = System::new();
37    system.refresh_processes_specifics(
38        ProcessesToUpdate::All,
39        true,
40        ProcessRefreshKind::everything(),
41    );
42
43    let mut processes = system
44        .processes()
45        .values()
46        .filter(|process| is_codex_like_process(process))
47        .map(process_info)
48        .collect::<Vec<_>>();
49    processes.sort_by_key(|process| process.pid);
50
51    FleetProcessScan {
52        processes,
53        error: None,
54    }
55}
56
57fn process_info(process: &sysinfo::Process) -> FleetProcessInfo {
58    FleetProcessInfo {
59        pid: pid_to_u32(process.pid()),
60        name: process.name().to_string_lossy().into_owned(),
61        command: command_line(process),
62        cwd: process.cwd().map(|path| path.display().to_string()),
63        started_at_ms: Some(process.start_time().saturating_mul(1_000)),
64    }
65}
66
67fn is_codex_like_process(process: &sysinfo::Process) -> bool {
68    let name = process.name().to_string_lossy().to_ascii_lowercase();
69    let command = process
70        .cmd()
71        .iter()
72        .map(|part| part.to_string_lossy().to_ascii_lowercase())
73        .collect::<Vec<_>>()
74        .join(" ");
75
76    is_codex_like_text(&name) || is_codex_like_text(&command)
77}
78
79fn is_codex_like_text(value: &str) -> bool {
80    value.contains("codex")
81        || value.contains("codex-helper")
82        || value.contains("openai-codex")
83        || value.contains("codex.exe")
84}
85
86fn command_line(process: &sysinfo::Process) -> Option<String> {
87    let parts = process
88        .cmd()
89        .iter()
90        .map(|part| part.to_string_lossy())
91        .filter(|part| !part.trim().is_empty())
92        .collect::<Vec<_>>();
93    if parts.is_empty() {
94        None
95    } else {
96        Some(parts.join(" "))
97    }
98}
99
100fn pid_to_u32(pid: Pid) -> u32 {
101    pid.as_u32()
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn codex_like_text_matches_expected_process_names() {
110        assert!(is_codex_like_text("codex"));
111        assert!(is_codex_like_text("codex-helper.exe"));
112        assert!(is_codex_like_text("openai-codex"));
113        assert!(!is_codex_like_text("cargo"));
114    }
115}