codex_helper_core/fleet/
process_scan.rs1use 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}